mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
refactor(pigx-common-security): 统一验证码校验逻辑并新增短信验证码前置校验
This commit is contained in:
1 parent
c796af6c5f
commit
5ad4dfb711
21 files changed
+564
-401
No files matched your search
+45
@@ -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<String, Object> redisTemplate) {
|
||||
return new CaptchaValidator(captchaService, redisTemplate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -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<String, Object> redisTemplate() {
|
||||
return SpringContextHolder.getBean(RedisTemplate.class);
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -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 三种类型。
|
||||
*
|
||||
* <p>
|
||||
* 调用方传入 {@code captchaType} 与 {@code captchaVerification},
|
||||
* math 类型的 {@code captchaVerification} 约定为 {@code randomStr---code},
|
||||
* 与前端组件 {@code SmsCodeVerifyButton} / {@code SmsCodeButton} 中的拼装格式一致。
|
||||
* </p>
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2026-05-20
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaValidator {
|
||||
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
private final RedisTemplate<String, Object> 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();
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* PIGX 验证码安全能力模块。
|
||||
*
|
||||
* <p>
|
||||
* 该包集中封装图形验证码与行为验证码的校验入口,统一处理 blockPuzzle、clickWord、math
|
||||
* 等验证码类型,避免认证服务、用户中心等业务模块重复拼装校验逻辑。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 同时提供 anji-captcha 的 Redis 缓存 SPI 实现,让行为验证码在多实例部署时共享缓存数据,
|
||||
* 保持验证码生成、校验和过期删除的一致性。
|
||||
* </p>
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2026-05-20
|
||||
*/
|
||||
package com.pig4cloud.pigx.common.security.captcha;
|
||||
+40
-44
@@ -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<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||||
return authorities.stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.filter(StringUtils::hasText)
|
||||
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
|
||||
}
|
||||
if (authentication == null) {
|
||||
return false;
|
||||
}
|
||||
Collection<? extends GrantedAuthority> 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<String> 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<String> scopeList = MapUtil.get(bearerTokenAuthentication.getTokenAttributes(),
|
||||
OAuth2ParameterNames.SCOPE, List.class, new ArrayList<>());
|
||||
return scopeList.stream().anyMatch(x -> PatternMatchUtils.simpleMatch(scopes, x));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
+106
-4
@@ -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<SysApiKey> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+13
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+1
-2
@@ -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() {
|
||||
|
||||
+9
@@ -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();
|
||||
|
||||
+30
-28
@@ -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
|
||||
*
|
||||
* <p>
|
||||
* 客户端异常处理 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<String> 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<String> 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));
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -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<Long> 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();
|
||||
}
|
||||
|
||||
}
|
||||
-34
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-5
@@ -32,9 +32,4 @@ public class PigxFeignClientConfiguration {
|
||||
return new PigxOAuthRequestInterceptor(tokenResolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RequestInterceptor clientToCRequestInterceptor() {
|
||||
return new PigxClientToCRequestInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -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();
|
||||
|
||||
+6
-6
@@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit;
|
||||
@RequiredArgsConstructor
|
||||
public class PigxRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final RedisTemplate<String, Object> 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) {
|
||||
|
||||
-142
@@ -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<RemoteAppUserService> 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<AppUserInfo> result = remoteAppUserServiceOptional.get().info(username);
|
||||
Optional<AppUserInfo> 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<AppUserInfo> 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<String> 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<? extends GrantedAuthority> 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);
|
||||
}
|
||||
|
||||
}
|
||||
-133
@@ -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<RemoteAppUserService> remoteAppUserServiceOptional;
|
||||
|
||||
/**
|
||||
* 用户密码登录
|
||||
*
|
||||
* @param phone 用户密码登录
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public UserDetails loadUserByUsername(String phone) {
|
||||
if (!remoteAppUserServiceOptional.isPresent()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
R<AppUserInfo> info = remoteAppUserServiceOptional.get().social(phone);
|
||||
return this.getUserDetailsAppUser(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUser(PigxUser pigxUser) {
|
||||
return pigxUser;
|
||||
}
|
||||
|
||||
UserDetails getUserDetailsAppUser(R<AppUserInfo> 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<String> 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<? extends GrantedAuthority> 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);
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
com.pig4cloud.pigx.common.security.captcha.CaptchaCacheServiceProvider
|
||||
+1
-3
@@ -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
|
||||
Reference in new issue
Block a user