From 5ad4dfb711b76503fa1ae538ef3b08b7bbbfe8cf Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Wed, 20 May 2026 23:24:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(pigx-common-security):=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E9=AA=8C=E8=AF=81=E7=A0=81=E6=A0=A1=E9=AA=8C=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E6=96=B0=E5=A2=9E=E7=9F=AD=E4=BF=A1=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E5=89=8D=E7=BD=AE=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../captcha/CaptchaAutoConfiguration.java | 45 ++++++ .../captcha/CaptchaCacheServiceProvider.java | 56 +++++++ .../security/captcha/CaptchaResult.java | 46 ++++++ .../security/captcha/CaptchaValidator.java | 82 ++++++++++ .../common/security/captcha/package-info.java | 17 +++ .../security/component/PermissionService.java | 84 +++++------ .../PigxCustomOpaqueTokenIntrospector.java | 110 +++++++++++++- .../PigxResourceServerAutoConfiguration.java | 13 ++ .../PigxResourceServerConfiguration.java | 3 +- .../component/PigxSecurityInnerAspect.java | 9 ++ .../ResourceAuthExceptionEntryPoint.java | 58 +++---- .../PigxSecurityUserContextHolder.java | 41 +++++ .../context/PigxUserContextAdapter.java | 64 ++++++++ .../PigxClientToCRequestInterceptor.java | 34 ----- .../feign/PigxFeignClientConfiguration.java | 5 - .../feign/PigxOAuthRequestInterceptor.java | 6 + ...edisOAuth2AuthorizationConsentService.java | 12 +- .../PigxTocDefaultUserDetailsServiceImpl.java | 142 ------------------ .../PigxTocMobileUserDetailsServiceImpl.java | 133 ---------------- ...m.anji.captcha.service.CaptchaCacheService | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 4 +- 21 files changed, 564 insertions(+), 401 deletions(-) create mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaAutoConfiguration.java create mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaCacheServiceProvider.java create mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaResult.java create mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java create mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/package-info.java create mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxSecurityUserContextHolder.java create mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxUserContextAdapter.java delete mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxClientToCRequestInterceptor.java delete mode 100755 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocDefaultUserDetailsServiceImpl.java delete mode 100644 pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocMobileUserDetailsServiceImpl.java create mode 100644 pigx-common/pigx-common-security/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaAutoConfiguration.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaAutoConfiguration.java new file mode 100644 index 000000000..c216aa70b --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaAutoConfiguration.java @@ -0,0 +1,45 @@ +package com.pig4cloud.pigx.common.security.captcha; + +import com.anji.captcha.service.CaptchaService; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * 验证码自动配置。仅在业务服务显式引入验证码依赖后注册验证码校验器。 + * + * @author lengleng + * @date 2026-05-20 + */ +@AutoConfigureAfter(name = "com.pig4cloud.pigx.common.data.cache.RedisTemplateConfiguration") +@ConditionalOnClass(name = {"com.anji.captcha.service.CaptchaService", "com.anji.captcha.model.vo.CaptchaVO"}) +@ConditionalOnBean(RedisTemplate.class) +@Configuration(proxyBeanMethods = false) +public class CaptchaAutoConfiguration { + + /** + * 验证码服务配置。仅在 anji-captcha 类型存在后再解析相关方法签名。 + */ + @Configuration(proxyBeanMethods = false) + static class CaptchaServiceConfiguration { + + /** + * 注册验证码统一校验器。 + * + * @param captchaService anji-captcha 校验服务 + * @param redisTemplate Redis 缓存操作模板 + * @return 验证码统一校验器 + */ + @Bean + @ConditionalOnMissingBean + CaptchaValidator captchaValidator(CaptchaService captchaService, RedisTemplate redisTemplate) { + return new CaptchaValidator(captchaService, redisTemplate); + } + + } + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaCacheServiceProvider.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaCacheServiceProvider.java new file mode 100644 index 000000000..066474fa6 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaCacheServiceProvider.java @@ -0,0 +1,56 @@ +package com.pig4cloud.pigx.common.security.captcha; + +import com.anji.captcha.service.CaptchaCacheService; +import com.pig4cloud.pigx.common.core.util.SpringContextHolder; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 行为验证码缓存的 Redis 实现,通过 SPI 加载替换 anji-captcha 默认本地实现, + * 让多实例部署共享同一份验证码缓存。 + * + * @author lengleng + * @date 2026-05-20 + */ +public class CaptchaCacheServiceProvider implements CaptchaCacheService { + + private static final String REDIS = "redis"; + + @Override + public void set(String key, String value, long expiresInSeconds) { + redisTemplate().opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); + } + + @Override + public boolean exists(String key) { + return Boolean.TRUE.equals(redisTemplate().hasKey(key)); + } + + @Override + public void delete(String key) { + redisTemplate().delete(key); + } + + @Override + public String get(String key) { + Object value = redisTemplate().opsForValue().get(key); + return value == null ? null : value.toString(); + } + + @Override + public String type() { + return REDIS; + } + + @Override + public Long increment(String key, long val) { + return redisTemplate().opsForValue().increment(key, val); + } + + @SuppressWarnings("unchecked") + private RedisTemplate redisTemplate() { + return SpringContextHolder.getBean(RedisTemplate.class); + } + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaResult.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaResult.java new file mode 100644 index 000000000..8fffe397d --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaResult.java @@ -0,0 +1,46 @@ +package com.pig4cloud.pigx.common.security.captcha; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 验证码校验结果。失败时携带提示文案,由上层决定包装成异常或 {@code R.failed}。 + * + * @author lengleng + * @date 2026-05-20 + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class CaptchaResult { + + /** + * 校验是否通过 + */ + private final boolean ok; + + /** + * 失败时的提示文案,校验成功时为空 + */ + private final String errorMessage; + + /** + * 构造验证码校验成功结果。 + * + * @return 成功结果,错误提示为空 + */ + public static CaptchaResult ok() { + return new CaptchaResult(true, null); + } + + /** + * 构造验证码校验失败结果。 + * + * @param errorMessage 失败提示文案 + * @return 失败结果 + */ + public static CaptchaResult failed(String errorMessage) { + return new CaptchaResult(false, errorMessage); + } + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java new file mode 100644 index 000000000..4c6a7936e --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java @@ -0,0 +1,82 @@ +package com.pig4cloud.pigx.common.security.captcha; + +import cn.hutool.core.util.StrUtil; +import com.anji.captcha.model.vo.CaptchaVO; +import com.anji.captcha.service.CaptchaService; +import com.pig4cloud.pigx.common.core.constant.CacheConstants; +import com.pig4cloud.pigx.common.core.constant.CommonConstants; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * 行为/图形验证码统一校验入口,支持 blockPuzzle、clickWord、math 三种类型。 + * + *

+ * 调用方传入 {@code captchaType} 与 {@code captchaVerification}, + * math 类型的 {@code captchaVerification} 约定为 {@code randomStr---code}, + * 与前端组件 {@code SmsCodeVerifyButton} / {@code SmsCodeButton} 中的拼装格式一致。 + *

+ * + * @author lengleng + * @date 2026-05-20 + */ +@RequiredArgsConstructor +public class CaptchaValidator { + + private final CaptchaService captchaService; + + private final RedisTemplate redisTemplate; + + /** + * 校验验证码。 + * + * @param captchaType 验证码类型,取值 blockPuzzle / clickWord / math + * @param captchaVerification 验证码凭据;math 类型为 randomStr---code + * @return 校验结果,失败时携带提示文案 + */ + public CaptchaResult validate(String captchaType, String captchaVerification) { + if (StrUtil.hasBlank(captchaType, captchaVerification)) { + return CaptchaResult.failed("验证码参数不能为空"); + } + + if (StrUtil.equalsAnyIgnoreCase(captchaType, CommonConstants.IMAGE_CODE_BLOCK_PUZZLE, + CommonConstants.IMAGE_CODE_CLICK_WORD)) { + CaptchaVO vo = new CaptchaVO(); + vo.setCaptchaType(captchaType); + vo.setCaptchaVerification(captchaVerification); + return captchaService.verification(vo).isSuccess() ? CaptchaResult.ok() + : CaptchaResult.failed("验证码错误或已过期"); + } + + if (StrUtil.equalsIgnoreCase(captchaType, CommonConstants.IMAGE_CODE_MATH)) { + return validateMath(captchaVerification); + } + + return CaptchaResult.failed("验证码类型不支持"); + } + + private CaptchaResult validateMath(String captchaVerification) { + String separator = CommonConstants.CAPTCHA_VERIFICATION_SEPARATOR; + int separatorIndex = captchaVerification.indexOf(separator); + if (separatorIndex < 0) { + return CaptchaResult.failed("验证码参数不能为空"); + } + + String randomStr = captchaVerification.substring(0, separatorIndex); + String code = captchaVerification.substring(separatorIndex + separator.length()); + if (StrUtil.hasBlank(randomStr, code)) { + return CaptchaResult.failed("验证码参数不能为空"); + } + + String key = CacheConstants.DEFAULT_CODE_KEY + randomStr; + Object codeObj = redisTemplate.opsForValue().get(key); + if (codeObj == null || StrUtil.isBlank(codeObj.toString()) || !StrUtil.equals(codeObj.toString(), code)) { + redisTemplate.delete(key); + return CaptchaResult.failed("验证码错误或已过期"); + } + + redisTemplate.delete(key); + return CaptchaResult.ok(); + } + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/package-info.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/package-info.java new file mode 100644 index 000000000..04ed51fb5 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/package-info.java @@ -0,0 +1,17 @@ +/** + * PIGX 验证码安全能力模块。 + * + *

+ * 该包集中封装图形验证码与行为验证码的校验入口,统一处理 blockPuzzle、clickWord、math + * 等验证码类型,避免认证服务、用户中心等业务模块重复拼装校验逻辑。 + *

+ * + *

+ * 同时提供 anji-captcha 的 Redis 缓存 SPI 实现,让行为验证码在多实例部署时共享缓存数据, + * 保持验证码生成、校验和过期删除的一致性。 + *

+ * + * @author lengleng + * @date 2026-05-20 + */ +package com.pig4cloud.pigx.common.security.captcha; diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PermissionService.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PermissionService.java index 4a817603a..605937bb5 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PermissionService.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PermissionService.java @@ -37,54 +37,50 @@ import java.util.List; */ public class PermissionService { - /** - * 判断接口是否有任意xxx,xxx权限 - * - * @param permissions 权限 - * @return {boolean} - */ - public boolean hasPermission(String... permissions) { - if (ArrayUtil.isEmpty(permissions)) { - return false; - } - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + /** + * 判断接口是否有任意xxx,xxx权限 + * @param permissions 权限 + * @return {boolean} + */ + public boolean hasPermission(String... permissions) { + if (ArrayUtil.isEmpty(permissions)) { + return false; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null) { - return false; - } - Collection authorities = authentication.getAuthorities(); - return authorities.stream() - .map(GrantedAuthority::getAuthority) - .filter(StringUtils::hasText) - .anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x)); - } + if (authentication == null) { + return false; + } + Collection authorities = authentication.getAuthorities(); + return authorities.stream() + .map(GrantedAuthority::getAuthority) + .filter(StringUtils::hasText) + .anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x)); + } - /** - * 客户端模式是否有该接口权限 - * - * @param scopes scope 列表 - * @return {boolean} - * @PreAuthorize("@pms.hasScope('server')") - */ - public boolean hasScope(String... scopes) { - if (ArrayUtil.isEmpty(scopes)) { - return false; - } + /** + * 客户端模式是否有该接口权限 + * @param scopes scope 列表 + * @return {boolean} @PreAuthorize("@pms.hasScope('server')") + */ + public boolean hasScope(String... scopes) { + if (ArrayUtil.isEmpty(scopes)) { + return false; + } - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null) { - return false; - } + if (authentication == null) { + return false; + } - - if (authentication instanceof BearerTokenAuthentication) { - BearerTokenAuthentication bearerTokenAuthentication = (BearerTokenAuthentication) authentication; - List scopeList = MapUtil.get(bearerTokenAuthentication.getTokenAttributes() - , OAuth2ParameterNames.SCOPE, List.class, new ArrayList<>()); - return scopeList.stream().anyMatch(x -> PatternMatchUtils.simpleMatch(scopes, x)); - } - return false; - } + if (authentication instanceof BearerTokenAuthentication) { + BearerTokenAuthentication bearerTokenAuthentication = (BearerTokenAuthentication) authentication; + List scopeList = MapUtil.get(bearerTokenAuthentication.getTokenAttributes(), + OAuth2ParameterNames.SCOPE, List.class, new ArrayList<>()); + return scopeList.stream().anyMatch(x -> PatternMatchUtils.simpleMatch(scopes, x)); + } + return false; + } } diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxCustomOpaqueTokenIntrospector.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxCustomOpaqueTokenIntrospector.java index 16128fc00..0d49fd7d4 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxCustomOpaqueTokenIntrospector.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxCustomOpaqueTokenIntrospector.java @@ -1,11 +1,20 @@ package com.pig4cloud.pigx.common.security.component; +import cn.hutool.core.util.StrUtil; +import com.pig4cloud.pigx.admin.api.entity.SysApiKey; +import com.pig4cloud.pigx.admin.api.feign.RemoteApiKeyService; +import com.pig4cloud.pigx.common.core.constant.CacheConstants; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; +import com.pig4cloud.pigx.common.core.util.R; import com.pig4cloud.pigx.common.core.util.SpringContextHolder; +import com.pig4cloud.pigx.common.core.util.WebUtils; +import com.pig4cloud.pigx.common.security.service.PigxDefaultUserDetailsServiceImpl; import com.pig4cloud.pigx.common.security.service.PigxUser; import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.core.Ordered; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; @@ -20,10 +29,9 @@ import org.springframework.security.oauth2.server.resource.InvalidBearerTokenExc import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import java.security.Principal; -import java.util.Comparator; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.CompletableFuture; /** * @author lengleng @@ -37,6 +45,11 @@ public class PigxCustomOpaqueTokenIntrospector implements OpaqueTokenIntrospecto @Override public OAuth2AuthenticatedPrincipal introspect(String token) { + // API-KEY 前缀检测:sk- 开头走独立处理逻辑 + if (token.startsWith("sk-")) { + return handleApiKeyToken(token); + } + OAuth2Authorization oldAuthorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); if (Objects.isNull(oldAuthorization)) { throw new InvalidBearerTokenException(token); @@ -96,4 +109,93 @@ public class PigxCustomOpaqueTokenIntrospector implements OpaqueTokenIntrospecto return pigxUser; } + /** + * 处理 API-KEY 类型的 token + * 1. 计算 SHA-256 哈希 + * 2. 查询 API-KEY 记录 + * 3. 校验状态、过期时间、IP白名单 + * 4. 加载用户信息并返回 + */ + private OAuth2AuthenticatedPrincipal handleApiKeyToken(String token) { + String hash = sha256(token); + + // 优先从缓存获取,避免远程调用 + CacheManager cacheManager = SpringContextHolder.getBean(CacheManager.class); + Cache cache = cacheManager.getCache(CacheConstants.API_KEY_DETAILS); + RemoteApiKeyService remoteApiKeyService = SpringContextHolder.getBean(RemoteApiKeyService.class); + SysApiKey apiKey = null; + if (cache != null && cache.get(hash) != null) { + apiKey = cache.get(hash, SysApiKey.class); + } + + if (apiKey == null) { + R result = remoteApiKeyService.getByHash(hash); + if (result == null || result.getData() == null) { + throw new InvalidBearerTokenException("Invalid API Key"); + } + apiKey = result.getData(); + // 回填缓存 + if (cache != null) { + cache.put(hash, apiKey); + } + } + + // 校验过期时间 + if (apiKey.getExpiresAt() != null && apiKey.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new InvalidBearerTokenException("API Key has expired"); + } + + // 校验 IP 白名单 + if (apiKey.getAllowedIps() != null && !apiKey.getAllowedIps().isEmpty()) { + String requestIp = WebUtils.getIP(); + if (requestIp == null || !isIpAllowed(requestIp, apiKey.getAllowedIps())) { + throw new InvalidBearerTokenException("IP not allowed for this API Key"); + } + } + + // 通过 apiKey 中的 username 直接加载用户详情,无需二次远程调用 + UserDetails userDetails = SpringContextHolder.getBean(PigxDefaultUserDetailsServiceImpl.class).loadUserByUsername(apiKey.getUsername()); + if (!(userDetails instanceof PigxUser pigxUser)) { + throw new InvalidBearerTokenException("API Key owner not found"); + } + + if (!pigxUser.isAccountNonLocked()) { + log.warn("用户账号 {} 已被锁定,拒绝 API Key 访问", pigxUser.getUsername()); + throw new InvalidBearerTokenException("Account is locked"); + } + if (!pigxUser.isAccountNonExpired()) { + log.warn("用户账号 {} 已过期,拒绝 API Key 访问", pigxUser.getUsername()); + throw new InvalidBearerTokenException("Account has expired"); + } + + // 异步更新最后使用时间,不阻塞主流程 + final Long apiKeyId = apiKey.getId(); + CompletableFuture.runAsync(() -> { + try { + remoteApiKeyService.updateLastUsed(apiKeyId); + } catch (Exception ex) { + log.warn("Failed to update API Key last_used_at: {}", ex.getMessage()); + } + }); + + pigxUser.getAttributes().put(SecurityConstants.CLIENT_ID, "apikey"); + return pigxUser; + } + + private boolean isIpAllowed(String requestIp, String allowedIps) { + return Arrays.stream(allowedIps.split(StrUtil.COMMA)) + .map(String::trim) + .anyMatch(requestIp::equals); + } + + private static String sha256(String input) { + try { + byte[] hash = java.security.MessageDigest.getInstance("SHA-256") + .digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } catch (java.security.NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } + } diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerAutoConfiguration.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerAutoConfiguration.java index 51950ee1d..d63f5e7e9 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerAutoConfiguration.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerAutoConfiguration.java @@ -17,7 +17,10 @@ package com.pig4cloud.pigx.common.security.component; import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.common.core.context.UserContextHolder; +import com.pig4cloud.pigx.common.security.context.PigxSecurityUserContextHolder; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; @@ -73,4 +76,14 @@ public class PigxResourceServerAutoConfiguration { return new PigxCustomOpaqueTokenIntrospector(authorizationService); } + /** + * 通用用户上下文 SPI 的安全实现,供 data / audit 等模块按接口注入使用。 + * @return UserContextHolder + */ + @Bean + @ConditionalOnMissingBean(UserContextHolder.class) + public UserContextHolder userContextHolder() { + return new PigxSecurityUserContextHolder(); + } + } diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerConfiguration.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerConfiguration.java index 7ea3506ad..0ed499347 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerConfiguration.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxResourceServerConfiguration.java @@ -22,7 +22,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.security.authorization.method.PrePostTemplateDefaults; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -95,7 +94,7 @@ public class PigxResourceServerConfiguration { /** * 创建并返回一个支持自定义权限表达式的默认模板实例 * - * @return {@link PrePostTemplateDefaults} 权限表达式默认模板实例 + * @return {@link AnnotationTemplateExpressionDefaults} 权限表达式默认模板实例 */ @Bean AnnotationTemplateExpressionDefaults prePostTemplateDefaults() { diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxSecurityInnerAspect.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxSecurityInnerAspect.java index ceb13612a..7ace7c81b 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxSecurityInnerAspect.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/PigxSecurityInnerAspect.java @@ -29,6 +29,10 @@ import org.aspectj.lang.annotation.Before; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import java.util.Objects; /** * @author lengleng @@ -46,6 +50,11 @@ public class PigxSecurityInnerAspect implements Ordered { @SneakyThrows @Before("@within(inner) || @annotation(inner)") public void around(JoinPoint point, Inner inner) { + RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); + if (Objects.isNull(attrs)) { + return; + } + String header = request.getHeader(SecurityConstants.FROM); if (inner == null) { Class clazz = point.getTarget().getClass(); diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/ResourceAuthExceptionEntryPoint.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/ResourceAuthExceptionEntryPoint.java index 5c301ac56..89688a027 100755 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/ResourceAuthExceptionEntryPoint.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/component/ResourceAuthExceptionEntryPoint.java @@ -20,6 +20,8 @@ import cn.hutool.http.ContentType; import com.fasterxml.jackson.databind.ObjectMapper; import com.pig4cloud.pigx.common.core.constant.CommonConstants; import com.pig4cloud.pigx.common.core.util.R; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.context.MessageSource; @@ -27,49 +29,49 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.web.AuthenticationEntryPoint; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.PrintWriter; /** * @author lengleng * @date 2019/2/1 - * + *

* 客户端异常处理 AuthenticationException 不同细化异常处理 */ @RequiredArgsConstructor public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint { - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; - private final MessageSource messageSource; + private final MessageSource messageSource; - @Override - @SneakyThrows - public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) { - response.setCharacterEncoding(CommonConstants.UTF8); - response.setContentType(ContentType.JSON.getValue()); - R result = new R<>(); - result.setCode(CommonConstants.FAIL); - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - if (authException != null) { - result.setMsg("error"); - result.setData(authException.getMessage()); - } + @Override + @SneakyThrows + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + response.setCharacterEncoding(CommonConstants.UTF8); + response.setContentType(ContentType.JSON.getValue()); + R result = new R<>(); + result.setCode(CommonConstants.FAIL); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + if (authException != null) { + result.setMsg("error"); + result.setData(authException.getMessage()); + } - // 针对令牌过期返回特殊的 424 - if (authException instanceof InvalidBearerTokenException - || authException instanceof InsufficientAuthenticationException) { - response.setStatus(HttpStatus.FAILED_DEPENDENCY.value()); - result.setMsg(this.messageSource.getMessage("OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired", - null, LocaleContextHolder.getLocale())); - } - PrintWriter printWriter = response.getWriter(); - printWriter.append(objectMapper.writeValueAsString(result)); - } + // 针对令牌过期返回特殊的 424 + if (authException instanceof InvalidBearerTokenException + || authException instanceof UsernameNotFoundException + || authException instanceof InsufficientAuthenticationException) { + response.setStatus(HttpStatus.FAILED_DEPENDENCY.value()); + result.setMsg(this.messageSource.getMessage("OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired", + null, LocaleContextHolder.getLocale())); + } + PrintWriter printWriter = response.getWriter(); + printWriter.append(objectMapper.writeValueAsString(result)); + } } diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxSecurityUserContextHolder.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxSecurityUserContextHolder.java new file mode 100644 index 000000000..e504538ff --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxSecurityUserContextHolder.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.common.security.context; + +import com.pig4cloud.pigx.common.core.context.UserContext; +import com.pig4cloud.pigx.common.core.context.UserContextHolder; +import com.pig4cloud.pigx.common.security.service.PigxUser; +import com.pig4cloud.pigx.common.security.util.SecurityUtils; + +/** + * 基于 Spring Security 上下文的 {@link UserContextHolder} 实现。 + * + * @author lengleng + */ +public class PigxSecurityUserContextHolder implements UserContextHolder { + + @Override + public UserContext get() { + PigxUser user = SecurityUtils.getUser(); + if (user == null) { + return null; + } + return new PigxUserContextAdapter(user); + } + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxUserContextAdapter.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxUserContextAdapter.java new file mode 100644 index 000000000..6797132bc --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/context/PigxUserContextAdapter.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.pig4cloud.pigx.common.security.context; + +import cn.hutool.core.util.StrUtil; +import com.pig4cloud.pigx.common.core.constant.SecurityConstants; +import com.pig4cloud.pigx.common.core.context.UserContext; +import com.pig4cloud.pigx.common.security.service.PigxUser; +import org.springframework.security.core.GrantedAuthority; + +import java.util.List; +import java.util.Objects; + +/** + * 将 {@link PigxUser} 适配为通用 {@link UserContext}, + * 从 authorities 中按 {@link SecurityConstants#ROLE} 前缀解析 roleIds, + * 与改造前 {@code PigxDefaultDataScopeHandle} 的行为保持一致。 + * + * @author lengleng + */ +public class PigxUserContextAdapter implements UserContext { + + private final PigxUser delegate; + + public PigxUserContextAdapter(PigxUser delegate) { + this.delegate = delegate; + } + + @Override + public String getUsername() { + return delegate.getUsername(); + } + + @Override + public Long getDeptId() { + return delegate.getDeptId(); + } + + @Override + public List getRoleIds() { + return delegate.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .filter(authority -> StrUtil.startWith(authority, SecurityConstants.ROLE)) + .map(authority -> Long.parseLong(Objects.requireNonNull(StrUtil.removePrefix(authority, SecurityConstants.ROLE)))) + .toList(); + } + +} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxClientToCRequestInterceptor.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxClientToCRequestInterceptor.java deleted file mode 100644 index 707bbcd91..000000000 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxClientToCRequestInterceptor.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.pig4cloud.pigx.common.security.feign; - -import cn.hutool.core.util.StrUtil; -import com.pig4cloud.pigx.common.core.constant.SecurityConstants; -import com.pig4cloud.pigx.common.core.util.WebUtils; -import feign.RequestInterceptor; -import feign.RequestTemplate; -import lombok.extern.slf4j.Slf4j; - -/** - * TOC 客户标识传递 - * - * @author lengleng - * @date 2023/3/17 - */ -@Slf4j -public class PigxClientToCRequestInterceptor implements RequestInterceptor { - - /** - * Called for every request. Add data using methods on the supplied - * {@link RequestTemplate}. - * @param template - */ - public void apply(RequestTemplate template) { - String reqVersion = WebUtils.getRequest() != null - ? WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC) : null; - - if (StrUtil.isNotBlank(reqVersion)) { - log.debug("feign add header toc :{}", reqVersion); - template.header(SecurityConstants.HEADER_TOC, reqVersion); - } - } - -} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxFeignClientConfiguration.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxFeignClientConfiguration.java index 5295620a1..ee95bc346 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxFeignClientConfiguration.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxFeignClientConfiguration.java @@ -32,9 +32,4 @@ public class PigxFeignClientConfiguration { return new PigxOAuthRequestInterceptor(tokenResolver); } - @Bean - public RequestInterceptor clientToCRequestInterceptor() { - return new PigxClientToCRequestInterceptor(); - } - } diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxOAuthRequestInterceptor.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxOAuthRequestInterceptor.java index 35cd240e8..efb0d8292 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxOAuthRequestInterceptor.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/feign/PigxOAuthRequestInterceptor.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; import com.pig4cloud.pigx.common.core.util.WebUtils; import com.pig4cloud.pigx.common.security.util.NonWebTokenContextHolder; +import com.pig4cloud.pigx.common.security.util.SecurityUtils; import feign.RequestInterceptor; import feign.RequestTemplate; import jakarta.servlet.http.HttpServletRequest; @@ -56,6 +57,11 @@ public class PigxOAuthRequestInterceptor implements RequestInterceptor { token = tokenResolver.resolve(request); } + // 从webcontext 中获取 token + if (StrUtil.isBlank(token)) { + token = SecurityUtils.getToken(); + } + // 如果 request 中没有 token,从 NonWebTokenContextHolder 获取(适配消息队列等非 Web 场景) if (StrUtil.isBlank(token)) { token = NonWebTokenContextHolder.getToken(); diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxRedisOAuth2AuthorizationConsentService.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxRedisOAuth2AuthorizationConsentService.java index 429cb4f8a..1fc90d7d3 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxRedisOAuth2AuthorizationConsentService.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxRedisOAuth2AuthorizationConsentService.java @@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit; @RequiredArgsConstructor public class PigxRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService { - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; private final static Long TIMEOUT = 10L; @@ -19,23 +19,23 @@ public class PigxRedisOAuth2AuthorizationConsentService implements OAuth2Authori public void save(OAuth2AuthorizationConsent authorizationConsent) { Assert.notNull(authorizationConsent, "authorizationConsent cannot be null"); - redisTemplate.opsForValue() - .set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT, TimeUnit.MINUTES); + redisTemplate.opsForValue() + .set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT, TimeUnit.MINUTES); } @Override public void remove(OAuth2AuthorizationConsent authorizationConsent) { Assert.notNull(authorizationConsent, "authorizationConsent cannot be null"); - redisTemplate.delete(buildKey(authorizationConsent)); + redisTemplate.delete(buildKey(authorizationConsent)); } @Override public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) { Assert.hasText(registeredClientId, "registeredClientId cannot be empty"); Assert.hasText(principalName, "principalName cannot be empty"); - return (OAuth2AuthorizationConsent) redisTemplate.opsForValue() - .get(buildKey(registeredClientId, principalName)); + return (OAuth2AuthorizationConsent) redisTemplate.opsForValue() + .get(buildKey(registeredClientId, principalName)); } private static String buildKey(String registeredClientId, String principalName) { diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocDefaultUserDetailsServiceImpl.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocDefaultUserDetailsServiceImpl.java deleted file mode 100755 index cef32c1f1..000000000 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocDefaultUserDetailsServiceImpl.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pig4cloud.pigx.common.security.service; - -import cn.hutool.core.util.ArrayUtil; -import com.pig4cloud.pigx.app.api.dto.AppUserInfo; -import com.pig4cloud.pigx.app.api.entity.AppUser; -import com.pig4cloud.pigx.app.api.feign.RemoteAppUserService; -import com.pig4cloud.pigx.common.core.constant.CacheConstants; -import com.pig4cloud.pigx.common.core.constant.CommonConstants; -import com.pig4cloud.pigx.common.core.constant.SecurityConstants; -import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum; -import com.pig4cloud.pigx.common.core.util.MsgUtils; -import com.pig4cloud.pigx.common.core.util.R; -import com.pig4cloud.pigx.common.core.util.RetOps; -import com.pig4cloud.pigx.common.core.util.WebUtils; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; - -import java.util.*; - -/** - * 用户详细信息 - * - * @author lengleng hccake - */ -@Slf4j -@RequiredArgsConstructor -public class PigxTocDefaultUserDetailsServiceImpl implements PigxUserDetailsService { - - private final CacheManager cacheManager; - - private final Optional remoteAppUserServiceOptional; - - /** - * 用户密码登录 - * - * @param username 用户密码登录 - * @return - */ - @Override - @SneakyThrows - public UserDetails loadUserByUsername(String username) { - Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS_MINI); - if (Objects.nonNull(cache) && Objects.nonNull(cache.get(username))) { - AppUserInfo appUserInfo = cache.get(username, AppUserInfo.class); - return getUserDetailsAppUser(R.ok(appUserInfo)); - } - - R result = remoteAppUserServiceOptional.get().info(username); - Optional userInfoOptional = RetOps.of(result).getData(); - userInfoOptional.ifPresent(userInfo -> Objects.requireNonNull(cache).put(username, userInfo)); - return getUserDetailsAppUser(result); - } - - @Override - public UserDetails loadUserByUser(PigxUser pigxUser) { - return pigxUser; - } - - UserDetails getUserDetailsAppUser(R result) { - // @formatter:off - return RetOps.of(result) - .assertSuccess(r -> new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE),null))) - .getData() - .map(this::convertUserDetailsAppUser) - .orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE))); - // @formatter:on - } - - /** - * UserInfo 转 UserDetails - * - * @param info - * @return 返回UserDetails对象 - */ - UserDetails convertUserDetailsAppUser(AppUserInfo info) { - Set dbAuthsSet = new HashSet<>(); - if (ArrayUtil.isNotEmpty(info.getRoles())) { - // 获取角色 - Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId)); - // 获取资源 - dbAuthsSet.addAll(Arrays.asList(info.getPermissions())); - - } - Collection authorities = AuthorityUtils - .createAuthorityList(dbAuthsSet.toArray(new String[0])); - AppUser user = info.getAppUser(); - // 构造security用户 - - return new PigxUser(user.getUserId(), user.getUsername(), null, null, null, null, user.getPhone(), user.getAvatar(), - user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(), - SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true, null, - !CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities); - } - - @Override - public int getOrder() { - return 10; - } - - /** - * 支持所有的 mobile 类型 - * - * @param clientId 目标客户端 - * @param grantType 授权类型 - * @return true/false - */ - @Override - public boolean support(String clientId, String grantType) { - if (Objects.isNull(WebUtils.getRequest())) { - return false; - } - String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC); - return SecurityConstants.HEADER_TOC_YES.equals(header); - } - -} diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocMobileUserDetailsServiceImpl.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocMobileUserDetailsServiceImpl.java deleted file mode 100644 index b168501b3..000000000 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/service/PigxTocMobileUserDetailsServiceImpl.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pig4cloud.pigx.common.security.service; - -import cn.hutool.core.util.ArrayUtil; -import com.pig4cloud.pigx.app.api.dto.AppUserInfo; -import com.pig4cloud.pigx.app.api.entity.AppUser; -import com.pig4cloud.pigx.app.api.feign.RemoteAppUserService; -import com.pig4cloud.pigx.common.core.constant.CommonConstants; -import com.pig4cloud.pigx.common.core.constant.SecurityConstants; -import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum; -import com.pig4cloud.pigx.common.core.util.MsgUtils; -import com.pig4cloud.pigx.common.core.util.R; -import com.pig4cloud.pigx.common.core.util.RetOps; -import com.pig4cloud.pigx.common.core.util.WebUtils; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; - -import java.util.*; - -/** - * 用户详细信息 - * - * @author lengleng hccake - */ -@Slf4j -@RequiredArgsConstructor -public class PigxTocMobileUserDetailsServiceImpl implements PigxUserDetailsService { - - private final Optional remoteAppUserServiceOptional; - - /** - * 用户密码登录 - * - * @param phone 用户密码登录 - * @return - */ - @Override - @SneakyThrows - public UserDetails loadUserByUsername(String phone) { - if (!remoteAppUserServiceOptional.isPresent()) { - throw new UnsupportedOperationException(); - } - R info = remoteAppUserServiceOptional.get().social(phone); - return this.getUserDetailsAppUser(info); - } - - @Override - public UserDetails loadUserByUser(PigxUser pigxUser) { - return pigxUser; - } - - UserDetails getUserDetailsAppUser(R result) { - // @formatter:off - return RetOps.of(result) - .assertSuccess(r -> new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE),null))) - .getData() - .map(this::convertUserDetailsAppUser) - .orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE))); - // @formatter:on - } - - /** - * UserInfo 转 UserDetails - * - * @param info - * @return 返回UserDetails对象 - */ - UserDetails convertUserDetailsAppUser(AppUserInfo info) { - Set dbAuthsSet = new HashSet<>(); - if (ArrayUtil.isNotEmpty(info.getRoles())) { - // 获取角色 - Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId)); - // 获取资源 - dbAuthsSet.addAll(Arrays.asList(info.getPermissions())); - - } - Collection authorities = AuthorityUtils - .createAuthorityList(dbAuthsSet.toArray(new String[0])); - AppUser user = info.getAppUser(); - // 构造security用户 - - return new PigxUser(user.getUserId(), user.getUsername(), null, null, null, null, user.getPhone(), user.getAvatar(), - user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(), - SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true, null, - !CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities); - } - - @Override - public int getOrder() { - return 15; - } - - /** - * 支持所有的 mobile 类型 - * - * @param clientId 目标客户端 - * @param grantType 授权类型 - * @return true/false - */ - @Override - public boolean support(String clientId, String grantType) { - if (Objects.isNull(WebUtils.getRequest())) { - return false; - } - - String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC); - return SecurityConstants.HEADER_TOC_YES.equals(header) && SecurityConstants.GRANT_MOBILE.equals(grantType); - } - -} diff --git a/pigx-common/pigx-common-security/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService b/pigx-common/pigx-common-security/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService new file mode 100644 index 000000000..80120f090 --- /dev/null +++ b/pigx-common/pigx-common-security/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaCacheService @@ -0,0 +1 @@ +com.pig4cloud.pigx.common.security.captcha.CaptchaCacheServiceProvider diff --git a/pigx-common/pigx-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/pigx-common/pigx-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 947141ae7..e75230f91 100644 --- a/pigx-common/pigx-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/pigx-common/pigx-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,7 +1,5 @@ com.pig4cloud.pigx.common.security.service.PigxDefaultUserDetailsServiceImpl -com.pig4cloud.pigx.common.security.service.PigxTocDefaultUserDetailsServiceImpl com.pig4cloud.pigx.common.security.service.PigxMobileUserDetailServiceImpl -com.pig4cloud.pigx.common.security.service.PigxTocMobileUserDetailsServiceImpl com.pig4cloud.pigx.common.security.service.PigxRedisOAuth2AuthorizationService com.pig4cloud.pigx.common.security.service.PigxRedisOAuth2AuthorizationConsentService com.pig4cloud.pigx.common.security.component.PigxBootCorsProperties @@ -9,4 +7,4 @@ com.pig4cloud.pigx.common.security.component.PigxSecurityInnerAspect com.pig4cloud.pigx.common.security.component.PigxInnerHeaderRemoveFilter com.pig4cloud.pigx.common.security.component.PigxSecurityMessageSourceConfiguration com.pig4cloud.pigx.common.security.service.PigxRemoteRegisteredClientRepository - +com.pig4cloud.pigx.common.security.captcha.CaptchaAutoConfiguration