refactor(security): Ports & Adapters 架构重构|解除反向依赖|抽离 Security Starter 准备
- 新增 2 个端口接口(UserAuthenticationPort / PermissionPort)+ 1 个适配器(PermissionAdapter) - SecurityUserDetails 用 roles(Set<String>) 替代 authorities,消除 Jackson 序列化问题 - 删除 UserSession,Redis / JWT 直存 SecurityUserDetails - SecurityUserDetailsService / PermissionService 注入 Port 替代 system Service
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
package com.youlai.boot.framework.security.config;
|
||||
package com.youlai.boot.auth.security.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import com.youlai.boot.framework.security.filter.CaptchaValidationFilter;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler;
|
||||
import com.youlai.boot.framework.security.handler.MyAuthenticationEntryPoint;
|
||||
import com.youlai.boot.framework.security.provider.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.framework.security.provider.WxMaAuthenticationProvider;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import com.youlai.boot.framework.security.service.SecurityUserDetailsService;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.framework.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import com.youlai.boot.auth.security.filter.CaptchaValidationFilter;
|
||||
import com.youlai.boot.auth.security.handler.JsonAccessDeniedHandler;
|
||||
import com.youlai.boot.auth.security.handler.JsonAuthenticationEntryPoint;
|
||||
import com.youlai.boot.auth.security.provider.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.auth.security.provider.WxMaAuthenticationProvider;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -29,9 +31,13 @@ import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
|
||||
/**
|
||||
* Spring Security 配置类
|
||||
* Spring Security 配置类。
|
||||
* <p>
|
||||
* 归使用方(auth 模块),安全规则(放行路径、CORS、Provider 装配、响应格式)
|
||||
* 因项目而异,不应由框架层强制装配。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
@@ -44,57 +50,41 @@ public class SecurityConfig {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
private final UserService userService;
|
||||
private final SysUserDetailsService userDetailsService;
|
||||
|
||||
private final SecurityUserDetailsService userDetailsService;
|
||||
private final CaptchaService captchaService;
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
/**
|
||||
* 配置安全过滤链 SecurityFilterChain
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(requestMatcherRegistry -> {
|
||||
// 配置无需登录即可访问的公开接口(配置文件方式)
|
||||
String[] ignoreUrls = securityProperties.getIgnoreUrls();
|
||||
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
|
||||
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
|
||||
}
|
||||
// 其他所有请求需登录后访问
|
||||
requestMatcherRegistry.anyRequest().authenticated();
|
||||
}
|
||||
)
|
||||
.exceptionHandling(configurer ->
|
||||
configurer
|
||||
.authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证异常处理器
|
||||
.accessDeniedHandler(new MyAccessDeniedHandler()) // 无权限访问异常处理器
|
||||
.authenticationEntryPoint(new JsonAuthenticationEntryPoint())
|
||||
.accessDeniedHandler(new JsonAccessDeniedHandler())
|
||||
)
|
||||
|
||||
// 禁用默认的 Spring Security 特性,适用于前后端分离架构
|
||||
.sessionManagement(configurer ->
|
||||
configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态认证,不使用 Session
|
||||
configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable) // 禁用 CSRF 防护,前后端分离无需此防护机制
|
||||
.formLogin(AbstractHttpConfigurer::disable) // 禁用默认的表单登录功能,前后端分离采用 Token 认证方式
|
||||
.httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录
|
||||
// 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
.httpBasic(AbstractHttpConfigurer::disable)
|
||||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||
// 验证码校验过滤器
|
||||
// 验证码校验(使用方过滤器,直接写 JSON 响应)
|
||||
.addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证和解析过滤器
|
||||
.addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
|
||||
// Token 认证(Starter 过滤器,抛 AuthenticationException 交给 ExceptionTranslationFilter 处理)
|
||||
.addFilterBefore(new TokenAuthenticationFilter(tokenManager), AuthorizationFilter.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置Web安全自定义器,以忽略特定请求路径的安全性检查。
|
||||
* <p>
|
||||
* 该配置用于指定哪些请求路径不经过Spring Security过滤器链。通常用于静态资源文件。
|
||||
*/
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> {
|
||||
@@ -105,38 +95,27 @@ public class SecurityConfig {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认密码认证的 Provider
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
return daoAuthenticationProvider;
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
*/
|
||||
@Bean
|
||||
public SmsAuthenticationProvider smsAuthenticationProvider() {
|
||||
return new SmsAuthenticationProvider(userService, redisTemplate);
|
||||
public SmsAuthenticationProvider smsAuthenticationProvider(UserAuthenticationPort userAuthPort) {
|
||||
return new SmsAuthenticationProvider(userAuthPort, redisTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序认证 Provider
|
||||
*/
|
||||
@Bean
|
||||
public WxMaAuthenticationProvider wechatMiniAuthenticationProvider(
|
||||
WxMaService wxMaService,
|
||||
SysUserDetailsService sysUserDetailsService
|
||||
UserAuthenticationPort userAuthenticationPort,
|
||||
UserSocialService userSocialService
|
||||
) {
|
||||
return new WxMaAuthenticationProvider(wxMaService, sysUserDetailsService);
|
||||
return new WxMaAuthenticationProvider(wxMaService, userAuthenticationPort, userSocialService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
DaoAuthenticationProvider daoAuthenticationProvider,
|
||||
@@ -149,5 +128,4 @@ public class SecurityConfig {
|
||||
wxMaAuthenticationProvider
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
package com.youlai.boot.framework.security.exception;
|
||||
package com.youlai.boot.auth.security.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* 需要绑定手机号异常
|
||||
* 需要绑定手机号异常(微信小程序登录未绑定手机号时抛出)。
|
||||
*/
|
||||
public class NeedBindMobileException extends AuthenticationException {
|
||||
public class MobileNotBoundException extends AuthenticationException {
|
||||
|
||||
private final String openid;
|
||||
|
||||
private final String sessionKey;
|
||||
|
||||
public NeedBindMobileException(String openid, String sessionKey) {
|
||||
public MobileNotBoundException(String openid, String sessionKey) {
|
||||
super("需要绑定手机号");
|
||||
this.openid = openid;
|
||||
this.sessionKey = sessionKey;
|
||||
@@ -24,5 +23,4 @@ public class NeedBindMobileException extends AuthenticationException {
|
||||
public String getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.exception;
|
||||
package com.youlai.boot.auth.security.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.springframework.security.core.AuthenticationException;
|
||||
* @since 2025/3/1
|
||||
*/
|
||||
public class SmsCaptchaException extends AuthenticationException {
|
||||
|
||||
public SmsCaptchaException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.filter;
|
||||
package com.youlai.boot.auth.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
@@ -29,7 +29,9 @@ import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 图形验证码校验过滤器
|
||||
* 图形验证码校验过滤器。
|
||||
* <p>
|
||||
* 归使用方,因为验证码规则(哪些接口需要验证码、验证码类型)因项目而异。
|
||||
*/
|
||||
public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
@@ -49,20 +51,17 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 非登录接口直接放行
|
||||
if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅支持 JSON 登录
|
||||
String contentType = request.getContentType();
|
||||
if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
|
||||
ResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// 包装请求,确保下游还能读取 body
|
||||
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request, -1);
|
||||
|
||||
byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream());
|
||||
@@ -85,9 +84,6 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper to allow repeated reads of the request body after we've parsed it here.
|
||||
*/
|
||||
private static class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] cachedBody;
|
||||
@@ -139,5 +135,3 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.handler;
|
||||
package com.youlai.boot.auth.security.handler;
|
||||
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.web.util.ResponseWriter;
|
||||
@@ -9,17 +9,18 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 无权限访问处理器
|
||||
* 无权限访问处理器。
|
||||
* <p>
|
||||
* 归使用方,因为 JSON 响应格式因项目而异。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class MyAccessDeniedHandler implements AccessDeniedHandler {
|
||||
public class JsonAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
|
||||
// 权限不足返回 403 Forbidden
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException) {
|
||||
ResponseWriter.writeError(response, ResultCode.ACCESS_PERMISSION_EXCEPTION);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.handler;
|
||||
package com.youlai.boot.auth.security.handler;
|
||||
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.web.util.ResponseWriter;
|
||||
@@ -14,35 +14,24 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 统一处理 Spring Security 认证失败响应
|
||||
* 统一处理 Spring Security 认证失败响应。
|
||||
* <p>
|
||||
* 归使用方,因为 JSON 响应格式(Result 结构、错误码)因项目而异。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
/**
|
||||
* 认证失败处理入口方法
|
||||
*
|
||||
* @param request 触发异常的请求对象(可用于获取请求头、参数等)
|
||||
* @param response 响应对象(用于写入错误信息)
|
||||
* @param authException 认证异常对象(包含具体失败原因)
|
||||
*/
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
if (authException instanceof BadCredentialsException) {
|
||||
// 用户名或密码错误
|
||||
ResponseWriter.writeError(response, ResultCode.USER_PASSWORD_ERROR);
|
||||
} else if(authException instanceof InsufficientAuthenticationException){
|
||||
// 请求头缺失Authorization、Token格式错误、Token过期、签名验证失败
|
||||
} else if (authException instanceof InsufficientAuthenticationException) {
|
||||
ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
} else {
|
||||
// 其他未明确处理的认证异常(如账户被锁定、账户禁用等)
|
||||
ResponseWriter.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.model;
|
||||
package com.youlai.boot.auth.security.model;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -8,13 +8,9 @@ import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Token
|
||||
* 短信验证码认证 Token。
|
||||
* <p>
|
||||
* 用于短信验证码登录场景,遵循 Spring Security 认证模型:
|
||||
* <ul>
|
||||
* <li>未认证状态:principal 为手机号,credentials 为验证码</li>
|
||||
* <li>已认证状态:principal 为用户详情,credentials 为 null</li>
|
||||
* </ul>
|
||||
* 未认证:principal=手机号,credentials=验证码;已认证:principal=SecurityUserDetails,credentials=null。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.20.0
|
||||
@@ -24,30 +20,9 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 621L;
|
||||
|
||||
/**
|
||||
* 认证信息
|
||||
* <ul>
|
||||
* <li>未认证时:手机号</li>
|
||||
* <li>已认证时:SysUserDetails 用户详情</li>
|
||||
* </ul>
|
||||
*/
|
||||
private final Object principal;
|
||||
|
||||
/**
|
||||
* 凭证信息
|
||||
* <ul>
|
||||
* <li>未认证时:短信验证码</li>
|
||||
* <li>已认证时:null</li>
|
||||
* </ul>
|
||||
*/
|
||||
private final Object credentials;
|
||||
|
||||
/**
|
||||
* 创建未认证的 Token
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param verifyCode 短信验证码
|
||||
*/
|
||||
public SmsAuthenticationToken(String mobile, String verifyCode) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.principal = mobile;
|
||||
@@ -55,12 +30,6 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已认证的 Token
|
||||
*
|
||||
* @param principal 用户详情(SysUserDetails)
|
||||
* @param authorities 授权信息
|
||||
*/
|
||||
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
@@ -68,13 +37,6 @@ public class SmsAuthenticationToken extends AbstractAuthenticationToken {
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已认证的 Token(静态工厂方法)
|
||||
*
|
||||
* @param principal 用户详情(SysUserDetails)
|
||||
* @param authorities 授权信息
|
||||
* @return 已认证的 SmsAuthenticationToken
|
||||
*/
|
||||
public static SmsAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new SmsAuthenticationToken(principal, authorities);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.model;
|
||||
package com.youlai.boot.auth.security.model;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -8,7 +8,9 @@ import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 微信小程序认证 Token
|
||||
* 微信小程序认证 Token。
|
||||
* <p>
|
||||
* 未认证:principal=微信code;已认证:principal=SecurityUserDetails。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.0.0
|
||||
@@ -18,25 +20,9 @@ public class WxMaAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 622L;
|
||||
|
||||
/**
|
||||
* 认证信息
|
||||
* 未认证时:微信code
|
||||
* 已认证时:SysUserDetails 用户详情
|
||||
*/
|
||||
private final Object principal;
|
||||
|
||||
/**
|
||||
* 凭证信息
|
||||
* 未认证时:null
|
||||
* 已认证时:null
|
||||
*/
|
||||
private final Object credentials;
|
||||
|
||||
/**
|
||||
* 创建未认证的 Token
|
||||
*
|
||||
* @param code 微信小程序code
|
||||
*/
|
||||
public WxMaAuthenticationToken(String code) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.principal = code;
|
||||
@@ -44,12 +30,6 @@ public class WxMaAuthenticationToken extends AbstractAuthenticationToken {
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已认证的 Token
|
||||
*
|
||||
* @param principal 用户详情(SysUserDetails)
|
||||
* @param authorities 授权信息
|
||||
*/
|
||||
public WxMaAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
@@ -57,9 +37,6 @@ public class WxMaAuthenticationToken extends AbstractAuthenticationToken {
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已认证的 Token(静态工厂方法)
|
||||
*/
|
||||
public static WxMaAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new WxMaAuthenticationToken(principal, authorities);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.youlai.boot.framework.security.provider;
|
||||
package com.youlai.boot.auth.security.provider;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.exception.SmsCaptchaException;
|
||||
import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import com.youlai.boot.auth.security.exception.SmsCaptchaException;
|
||||
import com.youlai.boot.auth.security.model.SmsAuthenticationToken;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
@@ -19,47 +19,34 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
* <p>
|
||||
* 实现 Spring Security 的 {@link AuthenticationProvider} 接口,处理短信验证码登录认证。
|
||||
* <p>
|
||||
* 认证流程:
|
||||
* <ol>
|
||||
* <li>根据手机号查询用户信息</li>
|
||||
* <li>校验用户状态(是否禁用)</li>
|
||||
* <li>校验用户状态</li>
|
||||
* <li>校验短信验证码(与 Redis 缓存比对)</li>
|
||||
* <li>验证成功后删除验证码,防止重复使用</li>
|
||||
* <li>验证成功后删除验证码</li>
|
||||
* <li>返回已认证的 Authentication</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.17.0
|
||||
* @see SmsAuthenticationToken
|
||||
* @see AuthenticationProvider
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final UserAuthenticationPort userAuthPort;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public SmsAuthenticationProvider(UserService userService, RedisTemplate<String, Object> redisTemplate) {
|
||||
this.userService = userService;
|
||||
public SmsAuthenticationProvider(UserAuthenticationPort userAuthPort, RedisTemplate<String, Object> redisTemplate) {
|
||||
this.userAuthPort = userAuthPort;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行短信验证码认证
|
||||
*
|
||||
* @param authentication 未认证的 {@link SmsAuthenticationToken}
|
||||
* @return 已认证的 {@link SmsAuthenticationToken}
|
||||
* @throws AuthenticationException 认证失败异常
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String mobile = (String) authentication.getPrincipal();
|
||||
String inputVerifyCode = (String) authentication.getCredentials();
|
||||
|
||||
// 参数校验
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
log.warn("短信验证码登录失败:手机号为空");
|
||||
throw new SmsCaptchaException("手机号不能为空");
|
||||
@@ -69,21 +56,18 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
throw new SmsCaptchaException("验证码不能为空");
|
||||
}
|
||||
|
||||
// 根据手机号获取用户信息
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByMobile(mobile);
|
||||
SecurityUser securityUser = userAuthPort.getAuthInfoByMobile(mobile);
|
||||
|
||||
if (userAuthInfo == null) {
|
||||
if (securityUser == null) {
|
||||
log.warn("短信验证码登录失败:用户不存在,手机号={}", mobile);
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
// 检查用户状态是否有效
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
log.warn("短信验证码登录失败:用户已禁用,用户名={}", userAuthInfo.getUsername());
|
||||
if (ObjectUtil.notEqual(securityUser.getStatus(), 1)) {
|
||||
log.warn("短信验证码登录失败:用户已禁用,用户名={}", securityUser.getUsername());
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 校验短信验证码
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
|
||||
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
@@ -97,24 +81,13 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
throw new SmsCaptchaException("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码,防止重复使用
|
||||
redisTemplate.delete(cacheKey);
|
||||
|
||||
// 构建认证后的用户详情信息
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
log.info("短信验证码登录成功:用户名={},手机号={}", userAuthInfo.getUsername(), mobile);
|
||||
|
||||
// 创建已认证的 SmsAuthenticationToken
|
||||
SecurityUserDetails userDetails = new SecurityUserDetails(securityUser);
|
||||
log.info("短信验证码登录成功:用户名={},手机号={}", securityUser.getUsername(), mobile);
|
||||
return SmsAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities());
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的认证类型
|
||||
*
|
||||
* @param authentication 认证类型
|
||||
* @return 是否支持该认证类型
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.youlai.boot.framework.security.provider;
|
||||
package com.youlai.boot.auth.security.provider;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.youlai.boot.framework.security.exception.NeedBindMobileException;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.framework.security.model.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import com.youlai.boot.auth.security.exception.MobileNotBoundException;
|
||||
import com.youlai.boot.auth.security.model.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
@@ -26,7 +28,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
public class WxMaAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
private final SysUserDetailsService sysUserDetailsService;
|
||||
private final UserAuthenticationPort userAuthPort;
|
||||
private final UserSocialService userSocialService;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
@@ -38,44 +41,35 @@ public class WxMaAuthenticationProvider implements AuthenticationProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 用 code 换取 openid
|
||||
WxMaJscode2SessionResult session = wxMaService.jsCode2SessionInfo(code);
|
||||
String openid = session.getOpenid();
|
||||
String sessionKey = session.getSessionKey();
|
||||
|
||||
log.info("微信小程序登录:openid={}", openid);
|
||||
|
||||
// 2. 根据 openid 查询绑定信息
|
||||
UserSocial userSocial = sysUserDetailsService.getWechatMiniBindInfo(openid);
|
||||
UserSocial userSocial = userSocialService.getByPlatformAndOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
|
||||
|
||||
if (userSocial == null) {
|
||||
// 未绑定,抛出异常提示需要绑定手机号
|
||||
log.info("微信小程序登录:用户未绑定手机号,openid={}", openid);
|
||||
throw new NeedBindMobileException(openid, sessionKey);
|
||||
throw new MobileNotBoundException(openid, sessionKey);
|
||||
}
|
||||
|
||||
// 3. 获取用户认证信息
|
||||
UserAuthInfo userAuthInfo = sysUserDetailsService.getAuthInfoByWechatOpenid(openid);
|
||||
SecurityUser securityUser = userAuthPort.getAuthInfoByOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
|
||||
|
||||
if (userAuthInfo == null) {
|
||||
if (securityUser == null) {
|
||||
log.warn("微信小程序登录失败:用户不存在,openid={}", openid);
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
// 4. 检查用户状态
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
log.warn("微信小程序登录失败:用户已禁用,username={}", userAuthInfo.getUsername());
|
||||
if (ObjectUtil.notEqual(securityUser.getStatus(), 1)) {
|
||||
log.warn("微信小程序登录失败:用户已禁用,username={}", securityUser.getUsername());
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 5. 更新 session_key
|
||||
sysUserDetailsService.updateWechatSessionKey(userSocial.getId(), sessionKey);
|
||||
|
||||
// 6. 构建已认证 Token
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
log.info("微信小程序登录成功:username={}, openid={}", userAuthInfo.getUsername(), openid);
|
||||
userSocialService.updateSessionKey(userSocial.getId(), sessionKey);
|
||||
|
||||
SecurityUserDetails userDetails = new SecurityUserDetails(securityUser);
|
||||
log.info("微信小程序登录成功:username={}, openid={}", securityUser.getUsername(), openid);
|
||||
return WxMaAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities());
|
||||
|
||||
} catch (WxErrorException e) {
|
||||
@@ -88,5 +82,4 @@ public class WxMaAuthenticationProvider implements AuthenticationProvider {
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return WxMaAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.auth.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
@@ -58,9 +58,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
// 2. 执行认证(认证中)
|
||||
// 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
|
||||
// 默认情况下会委托给 DaoAuthenticationProvider:
|
||||
// 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SysUserDetailsService 实现)
|
||||
// 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现)
|
||||
// 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
|
||||
// 认证通过后返回已认证的 Authentication(principal 为 SysUserDetails,authorities 为角色/权限集合)。
|
||||
// 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
|
||||
@@ -8,12 +8,12 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.vo.WxMaLoginVO;
|
||||
import com.youlai.boot.auth.service.WxMaAuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.exception.NeedBindMobileException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.auth.security.exception.MobileNotBoundException;
|
||||
import com.youlai.boot.auth.security.model.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.SysUser;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
@@ -69,7 +69,7 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
|
||||
.tokenType(authToken.getTokenType())
|
||||
.expiresIn(authToken.getExpiresIn())
|
||||
.build();
|
||||
} catch (NeedBindMobileException e) {
|
||||
} catch (MobileNotBoundException e) {
|
||||
return WxMaLoginVO.builder()
|
||||
.isNewUser(true)
|
||||
.needBindMobile(true)
|
||||
@@ -234,7 +234,7 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
|
||||
* 生成认证令牌
|
||||
*/
|
||||
private AuthenticationToken generateAuthToken(String mobile) {
|
||||
SysUserDetails userDetails = new SysUserDetails(userService.getAuthInfoByMobile(mobile));
|
||||
SecurityUserDetails userDetails = new SecurityUserDetails(userService.getAuthInfoByMobile(mobile));
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities()
|
||||
);
|
||||
|
||||
@@ -33,9 +33,9 @@ public interface JwtClaimConstants {
|
||||
String DATA_SCOPES = "dataScopes";
|
||||
|
||||
/**
|
||||
* 权限(角色Code)集合
|
||||
* 角色编码集合(不带 ROLE_ 前缀)
|
||||
*/
|
||||
String AUTHORITIES = "authorities";
|
||||
String ROLES = "roles";
|
||||
|
||||
/**
|
||||
* Token 版本号
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.system.enums;
|
||||
package com.youlai.boot.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.youlai.boot.common.base.IBaseEnum;
|
||||
@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.common.enums.DataScopeEnum;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -58,7 +58,7 @@ public class MyDataPermissionHandler implements DataPermissionHandler {
|
||||
|
||||
// 获取当前用户的数据权限列表
|
||||
List<RoleDataScope> dataScopes = SecurityUtils.getUser()
|
||||
.map(SysUserDetails::getDataScopes)
|
||||
.map(SecurityUserDetails::getDataScopes)
|
||||
.orElse(List.of());
|
||||
|
||||
// 如果任一角色是 ALL,则跳过数据权限过滤(并集策略)
|
||||
|
||||
@@ -2,78 +2,72 @@ package com.youlai.boot.framework.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.web.util.ResponseWriter;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Token 认证校验过滤器
|
||||
* Token 认证过滤器。
|
||||
* <p>
|
||||
* 仅负责解析 Token 和填充 {@link SecurityContextHolder}。
|
||||
* 无效 Token 抛出 {@link AuthenticationException},由使用方的
|
||||
* {@code AuthenticationEntryPoint} 统一处理响应格式。
|
||||
* <p>
|
||||
* 必须注册在 {@code ExceptionTranslationFilter} 之后(即 {@code AuthorizationFilter} 之前),
|
||||
* 这样抛出的异常才能被 {@code ExceptionTranslationFilter} 捕获。
|
||||
*
|
||||
* @author wangtao
|
||||
* @since 2025/3/6 16:50
|
||||
* @since 2025/3/6
|
||||
*/
|
||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* Token 管理器
|
||||
*/
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
public TokenAuthenticationFilter(TokenManager tokenManager) {
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Token ,包括验签和是否过期
|
||||
* 如果 Token 有效,将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String rawToken = resolveToken(request);
|
||||
|
||||
try {
|
||||
if (StrUtil.isNotBlank(rawToken)) {
|
||||
// 执行令牌有效性检查(包含密码学验签和过期时间验证)
|
||||
boolean isValidToken = tokenManager.validateToken(rawToken);
|
||||
if (!isValidToken) {
|
||||
ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
return;
|
||||
if (StrUtil.isNotBlank(rawToken)) {
|
||||
try {
|
||||
boolean isValid = tokenManager.validateToken(rawToken);
|
||||
if (!isValid) {
|
||||
SecurityContextHolder.clearContext();
|
||||
throw new InsufficientAuthenticationException("Token 无效或已过期");
|
||||
}
|
||||
|
||||
// 将令牌解析为 Spring Security 上下文认证对象
|
||||
Authentication authentication = tokenManager.parseToken(rawToken);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} catch (AuthenticationException ex) {
|
||||
SecurityContextHolder.clearContext();
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
SecurityContextHolder.clearContext();
|
||||
throw new InsufficientAuthenticationException("Token 认证失败", ex);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// 安全上下文清除保障(防止上下文残留)
|
||||
SecurityContextHolder.clearContext();
|
||||
ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续后续过滤器链执行
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中解析 Token(仅支持 Authorization Header)
|
||||
*/
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(authorizationHeader)
|
||||
&& authorizationHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
return authorizationHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(header) && header.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
return header.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户认证信息
|
||||
* 安全模块用户数据 POJO。
|
||||
* <p>
|
||||
* 用于登录认证过程中的用户信息承载,包含用户名、密码、状态、角色等与认证/授权相关的数据。
|
||||
* </p>
|
||||
* 作为端口接口的返回类型,承载用户认证所需的全部数据。
|
||||
* 纯 JDK 类型,无 system 模块依赖,可直接序列化。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2025/12/16
|
||||
*/
|
||||
@Data
|
||||
public class UserAuthInfo {
|
||||
public class SecurityUser {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
@@ -48,7 +48,7 @@ public class UserAuthInfo {
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 角色集合
|
||||
* 角色编码集合
|
||||
*/
|
||||
private Set<String> roles;
|
||||
|
||||
@@ -13,17 +13,18 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Spring Security 用户认证对象
|
||||
* Spring Security 用户认证对象。
|
||||
* <p>
|
||||
* 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。
|
||||
* 实现了 {@link UserDetails} 接口,提供用户的核心信息。
|
||||
* 实现 {@link UserDetails},封装用户标识、角色、数据权限等认证信息。
|
||||
* {@link #roles} 字段存储角色编码(不带 ROLE_ 前缀),
|
||||
* {@link #getAuthorities()} 运行时补前缀并转为 {@link SimpleGrantedAuthority}。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @version 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class SysUserDetails implements UserDetails {
|
||||
public class SecurityUserDetails implements UserDetails {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
@@ -52,42 +53,35 @@ public class SysUserDetails implements UserDetails {
|
||||
|
||||
/**
|
||||
* 数据权限列表
|
||||
* <p>
|
||||
* 存储用户所有角色的数据权限范围,用于实现多角色权限合并(并集策略)
|
||||
*/
|
||||
private List<RoleDataScope> dataScopes;
|
||||
|
||||
/**
|
||||
* 用户角色权限集合
|
||||
* 角色编码集合(不带 ROLE_ 前缀)
|
||||
*/
|
||||
private Collection<SimpleGrantedAuthority> authorities;
|
||||
private Set<String> roles;
|
||||
|
||||
/**
|
||||
* 构造函数:根据用户认证信息初始化用户详情对象
|
||||
*
|
||||
* @param user 用户认证信息对象 {@link UserAuthInfo}
|
||||
* 构造函数:根据 {@link SecurityUser} 初始化。
|
||||
*/
|
||||
public SysUserDetails(UserAuthInfo user) {
|
||||
public SecurityUserDetails(SecurityUser user) {
|
||||
this.userId = user.getUserId();
|
||||
this.username = user.getUsername();
|
||||
this.password = user.getPassword();
|
||||
this.enabled = ObjectUtil.equal(user.getStatus(), 1);
|
||||
this.deptId = user.getDeptId();
|
||||
this.dataScopes = user.getDataScopes();
|
||||
|
||||
// 初始化角色权限集合
|
||||
this.authorities = CollectionUtil.isNotEmpty(user.getRoles())
|
||||
? user.getRoles().stream()
|
||||
// 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add)
|
||||
.map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role))
|
||||
.collect(Collectors.toSet())
|
||||
: Collections.emptySet();
|
||||
this.roles = user.getRoles();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return this.authorities;
|
||||
if (CollectionUtil.isEmpty(roles)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return roles.stream()
|
||||
.map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,8 +101,6 @@ public class SysUserDetails implements UserDetails {
|
||||
|
||||
/**
|
||||
* 判断是否包含"全部数据"权限
|
||||
*
|
||||
* @return 是否有全部数据权限
|
||||
*/
|
||||
public boolean hasAllDataScope() {
|
||||
if (CollectionUtil.isEmpty(dataScopes)) {
|
||||
@@ -119,9 +111,7 @@ public class SysUserDetails implements UserDetails {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据权限列表
|
||||
*
|
||||
* @return 数据权限列表,永不为null
|
||||
* 获取数据权限列表,永不为 null
|
||||
*/
|
||||
public List<RoleDataScope> getDataScopes() {
|
||||
return dataScopes != null ? dataScopes : Collections.emptyList();
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.youlai.boot.framework.security.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户会话信息
|
||||
* <p>
|
||||
* 存储在Token中的用户会话快照,包含用户身份、数据权限和角色权限信息。
|
||||
* 用于Redis-Token模式下的会话管理,支持在线用户查询和会话控制。
|
||||
*
|
||||
* @author wangtao
|
||||
* @since 2025/2/27 10:31
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserSession {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 数据权限列表
|
||||
*/
|
||||
private List<RoleDataScope> dataScopes;
|
||||
|
||||
/**
|
||||
* 角色权限集合
|
||||
*/
|
||||
private Set<String> roles;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.youlai.boot.framework.security.port;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 权限查询端口。
|
||||
* <p>
|
||||
* 由 system 模块提供适配器实现,framework 层通过此接口获取角色权限集合,
|
||||
* 不直接依赖 system 模块的 {@code RoleMenuService}。
|
||||
*
|
||||
* @see com.youlai.boot.system.security.adapter.PermissionAdapter
|
||||
*/
|
||||
public interface PermissionPort {
|
||||
|
||||
/**
|
||||
* 根据角色编码集合查询权限标识集合。
|
||||
*
|
||||
* @param roleCodes 角色编码集合
|
||||
* @return 权限标识集合,如 "sys:user:create"
|
||||
*/
|
||||
Set<String> getRolePerms(Set<String> roleCodes);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.youlai.boot.framework.security.port;
|
||||
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
|
||||
/**
|
||||
* 用户认证信息查询端口。
|
||||
* <p>
|
||||
* 由 system 模块提供适配器实现,framework 层通过此接口获取认证数据,
|
||||
* 不直接依赖 system 模块的 {@code UserService} / {@code UserSocialService}。
|
||||
*
|
||||
* @see com.youlai.boot.system.security.adapter.UserAuthenticationAdapter
|
||||
*/
|
||||
public interface UserAuthenticationPort {
|
||||
|
||||
/**
|
||||
* 根据用户名查询认证信息。
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 认证信息,不存在返回 null
|
||||
*/
|
||||
SecurityUser getAuthInfoByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据手机号查询认证信息。
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @return 认证信息,不存在返回 null
|
||||
*/
|
||||
SecurityUser getAuthInfoByMobile(String mobile);
|
||||
|
||||
/**
|
||||
* 根据第三方平台 openid 查询认证信息。
|
||||
*
|
||||
* @param platform 第三方平台
|
||||
* @param openid openid
|
||||
* @return 认证信息,未绑定返回 null
|
||||
*/
|
||||
SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid);
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package com.youlai.boot.framework.security.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.framework.security.port.PermissionPort;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.service.RoleMenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -12,11 +12,10 @@ import org.springframework.util.PatternMatchUtils;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Spring Security 权限校验组件
|
||||
* Spring Security 权限校验组件。
|
||||
* <p>
|
||||
* 用于 SpEL 表达式权限校验,如:@PreAuthorize("@ss.hasPerm('sys:user:create')")
|
||||
* <p>
|
||||
* 权限数据来源:{@link RoleMenuService#getRolePermsByRoleCodes}(带 Redis 缓存)
|
||||
* 用于 SpEL 表达式:{@code @PreAuthorize("@ss.hasPerm('sys:user:create')")}。
|
||||
* 通过 {@link PermissionPort} 查询角色权限,不直接依赖 system 模块。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
@@ -26,39 +25,30 @@ import java.util.Set;
|
||||
@Slf4j
|
||||
public class PermissionService {
|
||||
|
||||
private final RoleMenuService roleMenuService;
|
||||
private final PermissionPort permissionPort;
|
||||
|
||||
/**
|
||||
* 判断当前登录用户是否拥有操作权限
|
||||
* <p>
|
||||
* 支持通配符匹配,如:权限码 "sys:user:*" 可匹配 "sys:user:create"、"sys:user:delete" 等
|
||||
*
|
||||
* @param requiredPerm 所需权限
|
||||
* @return 是否有权限
|
||||
* 判断当前用户是否拥有操作权限,支持通配符匹配。
|
||||
*/
|
||||
public boolean hasPerm(String requiredPerm) {
|
||||
if (StrUtil.isBlank(requiredPerm)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 超级管理员放行
|
||||
if (SecurityUtils.isRoot()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取当前登录用户的角色编码集合
|
||||
Set<String> roleCodes = SecurityUtils.getRoles();
|
||||
if (CollectionUtil.isEmpty(roleCodes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取当前登录用户的所有角色的权限列表(从缓存读取)
|
||||
Set<String> rolePerms = roleMenuService.getRolePermsByRoleCodes(roleCodes);
|
||||
Set<String> rolePerms = permissionPort.getRolePerms(roleCodes);
|
||||
if (CollectionUtil.isEmpty(rolePerms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断权限列表中是否包含所需权限(支持通配符)
|
||||
boolean hasPermission = rolePerms.stream()
|
||||
.anyMatch(rolePerm -> PatternMatchUtils.simpleMatch(rolePerm, requiredPerm));
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.youlai.boot.framework.security.service;
|
||||
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 系统用户认证 DetailsService。
|
||||
* <p>
|
||||
* 通过 {@link UserAuthenticationPort} 获取认证信息,不直接依赖 system 模块。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2021/10/19
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SecurityUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserAuthenticationPort userAuthPort;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
try {
|
||||
SecurityUser securityUser = userAuthPort.getAuthInfoByUsername(username);
|
||||
if (securityUser == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
return new SecurityUserDetails(securityUser);
|
||||
} catch (Exception e) {
|
||||
log.error("认证异常:{}", e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.youlai.boot.framework.security.service;
|
||||
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 系统用户认证 DetailsService
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2021/10/19
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SysUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserSocialService userSocialService;
|
||||
|
||||
/**
|
||||
* 根据用户名获取用户信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
* @throws UsernameNotFoundException 用户名未找到异常
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
try {
|
||||
UserAuthInfo userAuthInfo = userService.getAuthInfoByUsername(username);
|
||||
if (userAuthInfo == null) {
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
return new SysUserDetails(userAuthInfo);
|
||||
} catch (Exception e) {
|
||||
// 记录异常日志
|
||||
log.error("认证异常:{}", e.getMessage());
|
||||
// 抛出异常
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信小程序openid查询绑定信息
|
||||
*
|
||||
* @param openid 微信小程序openid
|
||||
* @return 绑定信息,未绑定返回null
|
||||
*/
|
||||
public UserSocial getWechatMiniBindInfo(String openid) {
|
||||
return userSocialService.getByPlatformAndOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信小程序openid获取用户认证信息
|
||||
*
|
||||
* @param openid 微信小程序openid
|
||||
* @return 用户认证信息,用户不存在返回null
|
||||
*/
|
||||
public UserAuthInfo getAuthInfoByWechatOpenid(String openid) {
|
||||
return userSocialService.getAuthInfoByOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新微信小程序session_key
|
||||
*
|
||||
* @param bindId 绑定记录ID
|
||||
* @param sessionKey session_key
|
||||
*/
|
||||
public void updateWechatSessionKey(Long bindId, String sessionKey) {
|
||||
userSocialService.updateSessionKey(bindId, sessionKey);
|
||||
}
|
||||
}
|
||||
@@ -17,30 +17,25 @@ import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit; // Import TimeUnit
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JWT Token 管理器
|
||||
* JWT Token 管理器。
|
||||
* <p>
|
||||
* 实现基于JWT的无状态认证,支持:
|
||||
* <ul>
|
||||
* <li>Access Token + Refresh Token 双令牌机制</li>
|
||||
* <li>Token 撤销(jti黑名单)</li>
|
||||
* <li>用户级会话失效(tokenVersion)</li>
|
||||
* <li>多角色数据权限存储</li>
|
||||
* </ul>
|
||||
* 基于 JWT 的无状态认证,支持 Access + Refresh 双令牌、Token 撤销(jti 黑名单)、
|
||||
* 用户级会话失效(tokenVersion)、多角色数据权限存储。
|
||||
* <p>
|
||||
* JWT claims 中存储角色编码(不带 ROLE_ 前缀),解析后由 {@link SecurityUserDetails#getAuthorities()} 运行时补前缀。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/11/15
|
||||
@@ -59,12 +54,6 @@ public class JwtTokenManager implements TokenManager {
|
||||
this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成令牌
|
||||
*
|
||||
* @param authentication 认证信息
|
||||
* @return 令牌响应对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken generateToken(Authentication authentication) {
|
||||
int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||
@@ -81,22 +70,15 @@ public class JwtTokenManager implements TokenManager {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析令牌
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return Authentication 对象
|
||||
*/
|
||||
@Override
|
||||
public Authentication parseToken(String token) {
|
||||
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
|
||||
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
|
||||
SecurityUserDetails userDetails = new SecurityUserDetails();
|
||||
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID));
|
||||
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID));
|
||||
|
||||
// 解析数据权限列表
|
||||
// 数据权限
|
||||
JSONArray dataScopesArray = payloads.getJSONArray(JwtClaimConstants.DATA_SCOPES);
|
||||
if (dataScopesArray != null && !dataScopesArray.isEmpty()) {
|
||||
List<RoleDataScope> dataScopes = dataScopesArray.stream()
|
||||
@@ -115,88 +97,60 @@ public class JwtTokenManager implements TokenManager {
|
||||
userDetails.setDataScopes(dataScopes);
|
||||
}
|
||||
|
||||
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
|
||||
// 角色集合
|
||||
Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES)
|
||||
.stream()
|
||||
.map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
|
||||
.collect(Collectors.toSet());
|
||||
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT));
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
|
||||
// 角色编码(不带 ROLE_ 前缀)
|
||||
JSONArray rolesArray = payloads.getJSONArray(JwtClaimConstants.ROLES);
|
||||
if (rolesArray != null && !rolesArray.isEmpty()) {
|
||||
Set<String> roles = rolesArray.stream()
|
||||
.map(Convert::toStr)
|
||||
.collect(Collectors.toSet());
|
||||
userDetails.setRoles(roles);
|
||||
}
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验令牌
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 是否有效
|
||||
*/
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
return validateToken(token, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验刷新令牌
|
||||
*
|
||||
* @param refreshToken JWT Token
|
||||
* @return 验证结果
|
||||
*/
|
||||
@Override
|
||||
public boolean validateRefreshToken(String refreshToken) {
|
||||
return validateToken(refreshToken, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验令牌
|
||||
* <p>
|
||||
* 校验流程(按顺序执行):
|
||||
* <ol>
|
||||
* <li>签名验证 + 过期时间检查</li>
|
||||
* <li>刷新令牌类型校验(仅刷新场景)</li>
|
||||
* <li>tokenVersion 校验(用户级会话失效)</li>
|
||||
* <li>jti 黑名单校验(单Token撤销)</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @param validateRefreshToken 是否校验刷新令牌类型
|
||||
* @return 是否有效
|
||||
*/
|
||||
private boolean validateToken(String token, boolean validateRefreshToken) {
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
// 检查 Token 是否有效(验签 + 是否过期)
|
||||
boolean isValid = jwt.setKey(secretKey).validate(0);
|
||||
|
||||
if (isValid) {
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
// 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
|
||||
// 刷新令牌类型校验
|
||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||
if (validateRefreshToken) {
|
||||
//刷新token需要校验token类别
|
||||
boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
|
||||
if (!isRefreshToken) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 2. 校验 tokenVersion(用于按用户维度失效历史 Token)
|
||||
// 场景示例:用户修改密码、被管理员强制下线、手动"踢所有端"后,递增 tokenVersion,
|
||||
// 之前签发的 Token 因版本号不匹配而失效
|
||||
|
||||
// tokenVersion 校验(用户维度 Token 失效)
|
||||
Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
|
||||
if (userId != null) {
|
||||
Integer tokenVersion = payloads.getInt(JwtClaimConstants.TOKEN_VERSION);
|
||||
|
||||
|
||||
String versionKey = StrUtil.format(RedisConstants.Auth.USER_TOKEN_VERSION, userId);
|
||||
Object currentVersionObj = redisTemplate.opsForValue().get(versionKey);
|
||||
int currentVersion = currentVersionObj != null ? Convert.toInt(currentVersionObj) : 0;
|
||||
|
||||
// 版本号不匹配则 Token 无效(新签发的 Token 版本号必须 >= Redis 中的版本号)
|
||||
if (tokenVersion == null || tokenVersion < currentVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 判断 Token 是否已被撤销(单端退出/会话注销)
|
||||
// 场景示例:单点退出登录、后台手动注销某个会话、封禁账号后立即阻断当前 Token 等
|
||||
// jti 黑名单校验
|
||||
if (isTokenRevoked(jti)) {
|
||||
return false;
|
||||
}
|
||||
@@ -204,17 +158,11 @@ public class JwtTokenManager implements TokenManager {
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将令牌加入黑名单
|
||||
*
|
||||
* @param token JWT Token
|
||||
*/
|
||||
@Override
|
||||
public void invalidateToken(String token) {
|
||||
if (StringUtils.isBlank(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
|
||||
}
|
||||
@@ -225,77 +173,15 @@ public class JwtTokenManager implements TokenManager {
|
||||
revokeTokenByJti(jti, expirationAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否已被撤销
|
||||
*
|
||||
* @param jti Token唯一标识
|
||||
* @return true-已撤销,false-未撤销
|
||||
*/
|
||||
private boolean isTokenRevoked(String jti) {
|
||||
if (StringUtils.isBlank(jti)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.REVOKED_JTI, jti)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Token加入撤销黑名单
|
||||
* <p>
|
||||
* 黑名单有效期与Token剩余有效期一致,避免永久存储
|
||||
*
|
||||
* @param jti Token唯一标识
|
||||
* @param expirationAt Token过期时间戳
|
||||
*/
|
||||
private void revokeTokenByJti(String jti, Integer expirationAt) {
|
||||
if (StringUtils.isBlank(jti)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String revokedJtiKey = StrUtil.format(RedisConstants.Auth.REVOKED_JTI, jti);
|
||||
if (expirationAt != null) {
|
||||
int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000);
|
||||
if (expirationAt < currentTimeSeconds) {
|
||||
return;
|
||||
}
|
||||
int expirationIn = expirationAt - currentTimeSeconds;
|
||||
redisTemplate.opsForValue().set(revokedJtiKey, Boolean.TRUE, expirationIn, TimeUnit.SECONDS);
|
||||
} else {
|
||||
redisTemplate.opsForValue().set(revokedJtiKey, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 失效指定用户的所有会话
|
||||
* <p>
|
||||
* 通过递增用户 tokenVersion,使该用户之前签发的所有 Token 因版本号不匹配而失效。
|
||||
* <p>
|
||||
* 适用场景:
|
||||
* <ul>
|
||||
* <li>用户修改密码</li>
|
||||
* <li>管理员强制下线用户</li>
|
||||
* <li>用户主动踢出所有设备</li>
|
||||
* <li>用户被禁用</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
@Override
|
||||
public void invalidateUserSessions(Long userId) {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String versionKey = StrUtil.format(RedisConstants.Auth.USER_TOKEN_VERSION, userId);
|
||||
// 递增版本号,无需设置 TTL(版本号永久有效,避免 TTL 过期导致的安全问题)
|
||||
redisTemplate.opsForValue().increment(versionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 令牌响应对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
boolean isValid = validateRefreshToken(refreshToken);
|
||||
@@ -313,45 +199,19 @@ public class JwtTokenManager implements TokenManager {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
*
|
||||
* @param authentication 认证信息
|
||||
* @param ttl 过期时间(秒),-1表示永不过期
|
||||
* @return JWT Token字符串
|
||||
*/
|
||||
// ======================== private ========================
|
||||
|
||||
private String generateToken(Authentication authentication, int ttl) {
|
||||
return generateToken(authentication, ttl, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
* <p>
|
||||
* Payload包含:
|
||||
* <ul>
|
||||
* <li>userId - 用户ID</li>
|
||||
* <li>deptId - 部门ID</li>
|
||||
* <li>dataScopes - 数据权限列表</li>
|
||||
* <li>authorities - 角色权限集合</li>
|
||||
* <li>tokenType - 是否为刷新令牌</li>
|
||||
* <li>tokenVersion - Token版本号(用于会话失效控制)</li>
|
||||
* <li>iat/exp - 签发/过期时间</li>
|
||||
* <li>jti - Token唯一标识(用于撤销)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param authentication 认证信息
|
||||
* @param ttl 过期时间(秒)
|
||||
* @param isRefreshToken 是否为刷新令牌
|
||||
* @return JWT Token字符串
|
||||
*/
|
||||
private String generateToken(Authentication authentication, int ttl, boolean isRefreshToken) {
|
||||
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||
SecurityUserDetails userDetails = (SecurityUserDetails) authentication.getPrincipal();
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID
|
||||
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
|
||||
payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId());
|
||||
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId());
|
||||
|
||||
// 存储数据权限列表
|
||||
// 数据权限
|
||||
List<RoleDataScope> dataScopes = userDetails.getDataScopes();
|
||||
if (dataScopes != null && !dataScopes.isEmpty()) {
|
||||
List<Map<String, Object>> scopesList = dataScopes.stream()
|
||||
@@ -366,13 +226,12 @@ public class JwtTokenManager implements TokenManager {
|
||||
payload.put(JwtClaimConstants.DATA_SCOPES, scopesList);
|
||||
}
|
||||
|
||||
// claims 中添加角色信息
|
||||
Set<String> roles = authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet());
|
||||
payload.put(JwtClaimConstants.AUTHORITIES, roles);
|
||||
// 角色编码(不带 ROLE_ 前缀)
|
||||
Set<String> roles = userDetails.getRoles();
|
||||
if (roles != null && !roles.isEmpty()) {
|
||||
payload.put(JwtClaimConstants.ROLES, roles);
|
||||
}
|
||||
|
||||
// 获取当前用户的 Token 版本号,用于会话失效控制
|
||||
Long userId = userDetails.getUserId();
|
||||
int tokenVersion = 0;
|
||||
if (userId != null) {
|
||||
@@ -384,12 +243,8 @@ public class JwtTokenManager implements TokenManager {
|
||||
|
||||
Date now = new Date();
|
||||
payload.put(JWTPayload.ISSUED_AT, now);
|
||||
payload.put(JwtClaimConstants.TOKEN_TYPE, false);
|
||||
if (isRefreshToken) {
|
||||
payload.put(JwtClaimConstants.TOKEN_TYPE, true);
|
||||
}
|
||||
payload.put(JwtClaimConstants.TOKEN_TYPE, isRefreshToken);
|
||||
|
||||
// 设置过期时间 -1 表示永不过期
|
||||
if (ttl != -1) {
|
||||
Date expiresAt = DateUtil.offsetSecond(now, ttl);
|
||||
payload.put(JWTPayload.EXPIRES_AT, expiresAt);
|
||||
@@ -400,4 +255,27 @@ public class JwtTokenManager implements TokenManager {
|
||||
return JWTUtil.createToken(payload, secretKey);
|
||||
}
|
||||
|
||||
private boolean isTokenRevoked(String jti) {
|
||||
if (StringUtils.isBlank(jti)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.REVOKED_JTI, jti)));
|
||||
}
|
||||
|
||||
private void revokeTokenByJti(String jti, Integer expirationAt) {
|
||||
if (StringUtils.isBlank(jti)) {
|
||||
return;
|
||||
}
|
||||
String revokedJtiKey = StrUtil.format(RedisConstants.Auth.REVOKED_JTI, jti);
|
||||
if (expirationAt != null) {
|
||||
int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000);
|
||||
if (expirationAt < currentTimeSeconds) {
|
||||
return;
|
||||
}
|
||||
int expirationIn = expirationAt - currentTimeSeconds;
|
||||
redisTemplate.opsForValue().set(revokedJtiKey, Boolean.TRUE, expirationIn, TimeUnit.SECONDS);
|
||||
} else {
|
||||
redisTemplate.opsForValue().set(revokedJtiKey, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.youlai.boot.framework.security.token;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
@@ -9,35 +8,25 @@ import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.UserSession;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Redis Token 管理器
|
||||
* Redis Token 管理器。
|
||||
* <p>
|
||||
* 实现基于Redis的有状态认证,支持:
|
||||
* <ul>
|
||||
* <li>Access Token + Refresh Token 双令牌机制</li>
|
||||
* <li>单设备/多设备登录控制</li>
|
||||
* <li>用户级会话失效</li>
|
||||
* <li>在线用户管理</li>
|
||||
* </ul>
|
||||
* 基于 Redis 的有状态 Token 认证,支持 Access + Refresh 双令牌、
|
||||
* 单/多设备登录控制、用户级会话失效。
|
||||
* <p>
|
||||
* 与JWT模式相比,Redis模式支持主动踢人、在线用户查询等功能
|
||||
* 直接存取 {@link SecurityUserDetails}(password 置 null),无需 UserSession 中间层。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/11/15
|
||||
@@ -58,33 +47,16 @@ public class RedisTokenManager implements TokenManager {
|
||||
this.jsonMapper = jsonMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Token
|
||||
*
|
||||
* @param authentication 用户认证信息
|
||||
* @return 生成的 AuthenticationToken 对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken generateToken(Authentication authentication) {
|
||||
SysUserDetails user = (SysUserDetails) authentication.getPrincipal();
|
||||
SecurityUserDetails user = (SecurityUserDetails) authentication.getPrincipal();
|
||||
String accessToken = IdUtil.fastSimpleUUID();
|
||||
String refreshToken = IdUtil.fastSimpleUUID();
|
||||
|
||||
// 构建用户会话信息
|
||||
UserSession userSession = new UserSession(
|
||||
user.getUserId(),
|
||||
user.getUsername(),
|
||||
user.getDeptId(),
|
||||
user.getDataScopes(),
|
||||
user.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
// 构建会话快照(不存密码)
|
||||
SecurityUserDetails sessionUser = buildSessionUser(user);
|
||||
|
||||
// 存储访问令牌、刷新令牌和刷新令牌映射
|
||||
storeTokensInRedis(accessToken, refreshToken, userSession);
|
||||
|
||||
// 单设备登录控制
|
||||
storeTokensInRedis(accessToken, refreshToken, sessionUser);
|
||||
handleSingleDeviceLogin(user.getUserId(), accessToken);
|
||||
|
||||
return AuthenticationToken.builder()
|
||||
@@ -94,61 +66,24 @@ public class RedisTokenManager implements TokenManager {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 token 解析用户信息
|
||||
*
|
||||
* @param token Redis Token
|
||||
* @return 构建的 Authentication 对象
|
||||
*/
|
||||
@Override
|
||||
public Authentication parseToken(String token) {
|
||||
Object raw = redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||
if (raw == null) return null;
|
||||
UserSession userSession = jsonMapper.convertValue(raw, UserSession.class);
|
||||
|
||||
// 构建用户权限集合
|
||||
Set<SimpleGrantedAuthority> authorities = null;
|
||||
|
||||
Set<String> roles = userSession.getRoles();
|
||||
if (CollectionUtil.isNotEmpty(roles)) {
|
||||
authorities = roles.stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
// 构建用户详情对象
|
||||
SysUserDetails userDetails = buildUserDetails(userSession, authorities);
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
|
||||
SecurityUserDetails userDetails = jsonMapper.convertValue(raw, SecurityUserDetails.class);
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Token 是否有效
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 是否有效
|
||||
*/
|
||||
@Override
|
||||
public boolean validateToken(String token) {
|
||||
return redisTemplate.hasKey(formatTokenKey(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 RefreshToken 是否有效
|
||||
*
|
||||
* @param refreshToken 访问令牌
|
||||
* @return 是否有效
|
||||
*/
|
||||
@Override
|
||||
public boolean validateRefreshToken(String refreshToken) {
|
||||
return redisTemplate.hasKey(formatRefreshTokenKey(refreshToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 新生成的 AuthenticationToken 对象
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
Object raw = redisTemplate.opsForValue()
|
||||
@@ -156,16 +91,16 @@ public class RedisTokenManager implements TokenManager {
|
||||
if (raw == null) {
|
||||
throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
UserSession userSession = jsonMapper.convertValue(raw, UserSession.class);
|
||||
Object oldAccessTokenValue = redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userSession.getUserId()));
|
||||
// 删除旧的访问令牌记录
|
||||
SecurityUserDetails sessionUser = jsonMapper.convertValue(raw, SecurityUserDetails.class);
|
||||
|
||||
Object oldAccessTokenValue = redisTemplate.opsForValue()
|
||||
.get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, sessionUser.getUserId()));
|
||||
Optional.of(oldAccessTokenValue)
|
||||
.map(String.class::cast)
|
||||
.ifPresent(oldAccessToken -> redisTemplate.delete(formatTokenKey(oldAccessToken)));
|
||||
|
||||
// 生成新访问令牌并存储
|
||||
String newAccessToken = IdUtil.fastSimpleUUID();
|
||||
storeAccessToken(newAccessToken, userSession);
|
||||
storeAccessToken(newAccessToken, sessionUser);
|
||||
|
||||
int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||
return AuthenticationToken.builder()
|
||||
@@ -175,164 +110,91 @@ public class RedisTokenManager implements TokenManager {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make access token invalid
|
||||
* <p>
|
||||
* Only deletes the current token, not all sessions for the user.
|
||||
* This ensures single-device logout doesn't affect other devices when allowMultiLogin=true.
|
||||
*
|
||||
* @param token Access token
|
||||
*/
|
||||
@Override
|
||||
public void invalidateToken(String token) {
|
||||
String cleanToken = cleanBearerPrefix(token);
|
||||
// Only delete the current token, not all user sessions
|
||||
redisTemplate.delete(formatTokenKey(cleanToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* 使指定用户的所有会话失效
|
||||
* <p>
|
||||
* 适用场景:用户修改密码、管理员强制下线、账号封禁等
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
@Override
|
||||
public void invalidateUserSessions(Long userId) {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 删除访问令牌相关
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
|
||||
Object accessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
||||
if (accessTokenValue instanceof String accessToken) {
|
||||
redisTemplate.delete(formatTokenKey(accessToken));
|
||||
}
|
||||
// 无论是否存在访问令牌映射,都尝试删除 userAccessKey
|
||||
redisTemplate.delete(userAccessKey);
|
||||
|
||||
// 2. 删除刷新令牌相关
|
||||
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
|
||||
Object refreshTokenValue = redisTemplate.opsForValue().get(userRefreshKey);
|
||||
if (refreshTokenValue instanceof String refreshToken) {
|
||||
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
|
||||
}
|
||||
// 同样清理 userRefreshKey 本身
|
||||
redisTemplate.delete(userRefreshKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将访问令牌和刷新令牌存储至 Redis
|
||||
*
|
||||
* @param accessToken 访问令牌
|
||||
* @param refreshToken 刷新令牌
|
||||
* @param userSession 用户会话信息
|
||||
*/
|
||||
private void storeTokensInRedis(String accessToken, String refreshToken, UserSession userSession) {
|
||||
// 访问令牌 -> 用户信息
|
||||
setRedisValue(formatTokenKey(accessToken), userSession, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
// ======================== private ========================
|
||||
|
||||
// 刷新令牌 -> 用户信息
|
||||
String refreshTokenKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
|
||||
setRedisValue(refreshTokenKey, userSession, securityProperties.getSession().getRefreshTokenTimeToLive());
|
||||
|
||||
// 用户ID -> 刷新令牌
|
||||
setRedisValue(StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userSession.getUserId()),
|
||||
refreshToken,
|
||||
securityProperties.getSession().getRefreshTokenTimeToLive());
|
||||
private SecurityUserDetails buildSessionUser(SecurityUserDetails user) {
|
||||
SecurityUserDetails sessionUser = new SecurityUserDetails();
|
||||
sessionUser.setUserId(user.getUserId());
|
||||
sessionUser.setUsername(user.getUsername());
|
||||
sessionUser.setDeptId(user.getDeptId());
|
||||
sessionUser.setDataScopes(user.getDataScopes());
|
||||
sessionUser.setRoles(user.getRoles());
|
||||
sessionUser.setEnabled(user.isEnabled());
|
||||
sessionUser.setPassword(null);
|
||||
return sessionUser;
|
||||
}
|
||||
|
||||
private void storeTokensInRedis(String accessToken, String refreshToken, SecurityUserDetails sessionUser) {
|
||||
setRedisValue(formatTokenKey(accessToken), sessionUser,
|
||||
securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
String refreshTokenKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
|
||||
setRedisValue(refreshTokenKey, sessionUser,
|
||||
securityProperties.getSession().getRefreshTokenTimeToLive());
|
||||
setRedisValue(StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, sessionUser.getUserId()),
|
||||
refreshToken, securityProperties.getSession().getRefreshTokenTimeToLive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单设备登录控制
|
||||
* <p>
|
||||
* 当配置不允许多设备登录时,新登录会使旧Token失效
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param accessToken 新生成的访问令牌
|
||||
*/
|
||||
private void handleSingleDeviceLogin(Long userId, String accessToken) {
|
||||
Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin();
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
|
||||
// 单设备登录控制,删除旧的访问令牌
|
||||
if (!allowMultiLogin) {
|
||||
Object oldAccessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
||||
if (oldAccessTokenValue instanceof String oldAccessToken) {
|
||||
redisTemplate.delete(formatTokenKey(oldAccessToken));
|
||||
}
|
||||
}
|
||||
// 存储访问令牌映射(用户ID -> 访问令牌),用于单设备登录控制删除旧的访问令牌和刷新令牌时删除旧令牌
|
||||
setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储新的访问令牌
|
||||
*
|
||||
* @param newAccessToken 新访问令牌
|
||||
* @param userSession 用户会话信息
|
||||
*/
|
||||
private void storeAccessToken(String newAccessToken, UserSession userSession) {
|
||||
setRedisValue(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), userSession, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userSession.getUserId());
|
||||
private void storeAccessToken(String newAccessToken, SecurityUserDetails sessionUser) {
|
||||
setRedisValue(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), sessionUser,
|
||||
securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, sessionUser.getUserId());
|
||||
setRedisValue(userAccessKey, newAccessToken, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户详情对象
|
||||
*
|
||||
* @param userSession 用户会话信息
|
||||
* @param authorities 权限集合
|
||||
* @return SysUserDetails 用户详情
|
||||
*/
|
||||
private SysUserDetails buildUserDetails(UserSession userSession, Set<SimpleGrantedAuthority> authorities) {
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(userSession.getUserId());
|
||||
userDetails.setUsername(userSession.getUsername());
|
||||
userDetails.setDeptId(userSession.getDeptId());
|
||||
userDetails.setDataScopes(userSession.getDataScopes());
|
||||
userDetails.setAuthorities(authorities);
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化访问令牌的 Redis 键
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @return 格式化后的 Redis 键
|
||||
*/
|
||||
private String formatTokenKey(String token) {
|
||||
return StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化刷新令牌的 Redis 键
|
||||
*
|
||||
* @param refreshToken 访问令牌
|
||||
* @return 格式化后的 Redis 键
|
||||
*/
|
||||
private String formatRefreshTokenKey(String refreshToken) {
|
||||
return StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值存储到 Redis
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 过期时间(秒),-1表示永不过期
|
||||
*/
|
||||
private void setRedisValue(String key, Object value, int ttl) {
|
||||
if (ttl != -1) {
|
||||
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
|
||||
} else {
|
||||
redisTemplate.opsForValue().set(key, value); // ttl=-1时永不过期
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清理 Bearer 前缀
|
||||
*/
|
||||
private String cleanBearerPrefix(String token) {
|
||||
if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
return token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()).trim();
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
package com.youlai.boot.framework.security.util;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.constant.SystemConstants;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Spring Security 工具类
|
||||
@@ -26,101 +21,73 @@ import java.util.stream.Collectors;
|
||||
public class SecurityUtils {
|
||||
|
||||
/**
|
||||
* 获取当前登录人信息
|
||||
*
|
||||
* @return Optional<SysUserDetails>
|
||||
* 获取当前登录用户
|
||||
*/
|
||||
public static Optional<SysUserDetails> getUser() {
|
||||
public static Optional<SecurityUserDetails> getUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null) {
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof SysUserDetails) {
|
||||
return Optional.of((SysUserDetails) principal);
|
||||
if (principal instanceof SecurityUserDetails) {
|
||||
return Optional.of((SecurityUserDetails) principal);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户ID
|
||||
*
|
||||
* @return Long
|
||||
*/
|
||||
public static Long getUserId() {
|
||||
return getUser().map(SysUserDetails::getUserId).orElse(null);
|
||||
return getUser().map(SecurityUserDetails::getUserId).orElse(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户账号
|
||||
*
|
||||
* @return String 用户账号
|
||||
*/
|
||||
public static String getUsername() {
|
||||
return getUser().map(SysUserDetails::getUsername).orElse(null);
|
||||
return getUser().map(SecurityUserDetails::getUsername).orElse(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取部门ID
|
||||
*
|
||||
* @return Long
|
||||
*/
|
||||
public static Long getDeptId() {
|
||||
return getUser().map(SysUserDetails::getDeptId).orElse(null);
|
||||
return getUser().map(SecurityUserDetails::getDeptId).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据权限列表
|
||||
*
|
||||
* @return 数据权限列表
|
||||
*/
|
||||
public static List<RoleDataScope> getDataScopes() {
|
||||
return getUser().map(SysUserDetails::getDataScopes).orElse(List.of());
|
||||
return getUser().map(SecurityUserDetails::getDataScopes).orElse(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色集合
|
||||
*
|
||||
* @return 角色集合
|
||||
* 获取角色编码集合,不带 ROLE_ 前缀。
|
||||
* <p>
|
||||
* 直接从 {@link SecurityUserDetails#getRoles()} 取值,不做 stripping 和过滤。
|
||||
*/
|
||||
public static Set<String> getRoles() {
|
||||
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
|
||||
.map(Authentication::getAuthorities)
|
||||
.filter(CollectionUtil::isNotEmpty)
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
// 筛选角色,authorities 中的角色都是以 ROLE_ 开头
|
||||
.filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX))
|
||||
.map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX))
|
||||
.collect(Collectors.toSet());
|
||||
return getUser().map(SecurityUserDetails::getRoles).orElse(Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否超级管理员
|
||||
* <p>
|
||||
* 超级管理员忽视任何权限判断
|
||||
*/
|
||||
public static boolean isRoot() {
|
||||
Set<String> roles = getRoles();
|
||||
return roles.contains(SystemConstants.ROOT_ROLE_CODE);
|
||||
return getRoles().contains(SystemConstants.ROOT_ROLE_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求中的 Token
|
||||
*
|
||||
* @return Token 字符串
|
||||
* 从请求头获取 Token
|
||||
*/
|
||||
public static String getAccessToken() {
|
||||
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
|
||||
if(Objects.isNull(servletRequestAttributes)) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request = servletRequestAttributes.getRequest();
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
return request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.message.controller;
|
||||
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.message.service.SseService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -29,7 +29,7 @@ public class SseController {
|
||||
@Operation(summary = "建立SSE连接")
|
||||
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter connect() {
|
||||
SysUserDetails user = SecurityUtils.getUser().orElse(null);
|
||||
SecurityUserDetails user = SecurityUtils.getUser().orElse(null);
|
||||
if (user == null) {
|
||||
log.warn("SSE连接失败:未获取到当前用户");
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.youlai.boot.system.model.entity.SysUser;
|
||||
import com.youlai.boot.system.model.query.UserQuery;
|
||||
import com.youlai.boot.system.model.form.UserForm;
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.system.model.vo.UserExportVO;
|
||||
import com.youlai.boot.system.model.vo.UserPageVO;
|
||||
import com.youlai.boot.system.model.vo.UserProfileVO;
|
||||
@@ -48,9 +48,9 @@ public interface UserMapper extends BaseMapper<SysUser> {
|
||||
* @param username 用户名
|
||||
* @return 认证信息
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByUsername(String username);
|
||||
SecurityUser getAuthInfoByUsername(String username);
|
||||
|
||||
default UserAuthInfo getAuthCredentialsByUsername(String username) {
|
||||
default SecurityUser getAuthCredentialsByUsername(String username) {
|
||||
return getAuthInfoByUsername(username);
|
||||
}
|
||||
|
||||
@@ -60,9 +60,9 @@ public interface UserMapper extends BaseMapper<SysUser> {
|
||||
* @param mobile 手机号
|
||||
* @return 认证信息
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByMobile(String mobile);
|
||||
SecurityUser getAuthInfoByMobile(String mobile);
|
||||
|
||||
default UserAuthInfo getAuthCredentialsByMobile(String mobile) {
|
||||
default SecurityUser getAuthCredentialsByMobile(String mobile) {
|
||||
return getAuthInfoByMobile(mobile);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.system.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@@ -17,6 +17,6 @@ public interface UserSocialMapper extends BaseMapper<UserSocial> {
|
||||
* @param userId 用户ID
|
||||
* @return 认证信息
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByUserId(Long userId);
|
||||
SecurityUser getAuthInfoByUserId(Long userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.youlai.boot.system.security.adapter;
|
||||
|
||||
import com.youlai.boot.framework.security.port.PermissionPort;
|
||||
import com.youlai.boot.system.service.RoleMenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 权限查询适配器。
|
||||
* <p>
|
||||
* 实现 framework 层的 {@link PermissionPort},委托 {@link RoleMenuService} 查询权限集合。
|
||||
*
|
||||
* @see PermissionPort
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PermissionAdapter implements PermissionPort {
|
||||
|
||||
private final RoleMenuService roleMenuService;
|
||||
|
||||
@Override
|
||||
public Set<String> getRolePerms(Set<String> roleCodes) {
|
||||
return roleMenuService.getRolePermsByRoleCodes(roleCodes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.youlai.boot.system.security.adapter;
|
||||
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 用户认证信息查询适配器。
|
||||
* <p>
|
||||
* 实现 framework 层的 {@link UserAuthenticationPort},委托 system 层服务完成查询。
|
||||
*
|
||||
* @see UserAuthenticationPort
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserAuthenticationAdapter implements UserAuthenticationPort {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserSocialService userSocialService;
|
||||
|
||||
@Override
|
||||
public SecurityUser getAuthInfoByUsername(String username) {
|
||||
return userService.getAuthInfoByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecurityUser getAuthInfoByMobile(String mobile) {
|
||||
return userService.getAuthInfoByMobile(mobile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) {
|
||||
return userSocialService.getAuthInfoByOpenid(platform, openid);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.system.service;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.system.model.vo.CurrentUserVO;
|
||||
import com.youlai.boot.system.model.vo.UserExportVO;
|
||||
import com.youlai.boot.system.model.entity.SysUser;
|
||||
@@ -78,11 +78,11 @@ public interface UserService extends IService<SysUser> {
|
||||
* 根据用户名获取认证信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return {@link UserAuthInfo}
|
||||
* @return {@link SecurityUser}
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByUsername(String username);
|
||||
SecurityUser getAuthInfoByUsername(String username);
|
||||
|
||||
default UserAuthInfo getAuthCredentialsByUsername(String username) {
|
||||
default SecurityUser getAuthCredentialsByUsername(String username) {
|
||||
return getAuthInfoByUsername(username);
|
||||
}
|
||||
|
||||
@@ -194,11 +194,11 @@ public interface UserService extends IService<SysUser> {
|
||||
* 根据手机号获取用户认证信息
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @return {@link UserAuthInfo}
|
||||
* @return {@link SecurityUser}
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByMobile(String mobile);
|
||||
SecurityUser getAuthInfoByMobile(String mobile);
|
||||
|
||||
default UserAuthInfo getAuthCredentialsByMobile(String mobile) {
|
||||
default SecurityUser getAuthCredentialsByMobile(String mobile) {
|
||||
return getAuthInfoByMobile(mobile);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.youlai.boot.system.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
|
||||
/**
|
||||
@@ -57,7 +57,7 @@ public interface UserSocialService extends IService<UserSocial> {
|
||||
* @param openid openid
|
||||
* @return 用户认证信息
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByOpenid(SocialPlatformEnum platform, String openid);
|
||||
SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid);
|
||||
|
||||
/**
|
||||
* 更新session_key
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.youlai.boot.framework.integration.mail.service.MailService;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.converter.UserConverter;
|
||||
@@ -206,39 +206,34 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
|
||||
* 根据用户名获取认证凭证信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户认证凭证信息 {@link UserAuthInfo}
|
||||
* @return 用户认证凭证信息 {@link SecurityUser}
|
||||
*/
|
||||
@Override
|
||||
public UserAuthInfo getAuthInfoByUsername(String username) {
|
||||
UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByUsername(username);
|
||||
if (userAuthInfo != null) {
|
||||
Set<String> roles = userAuthInfo.getRoles();
|
||||
// 获取数据权限列表(用于并集策略)
|
||||
public SecurityUser getAuthInfoByUsername(String username) {
|
||||
SecurityUser securityUser = this.baseMapper.getAuthInfoByUsername(username);
|
||||
if (securityUser != null) {
|
||||
Set<String> roles = securityUser.getRoles();
|
||||
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
|
||||
userAuthInfo.setDataScopes(dataScopes);
|
||||
securityUser.setDataScopes(dataScopes);
|
||||
}
|
||||
return userAuthInfo;
|
||||
return securityUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号获取用户认证信息
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @return 用户认证信息
|
||||
*/
|
||||
@Override
|
||||
public UserAuthInfo getAuthInfoByMobile(String mobile) {
|
||||
public SecurityUser getAuthInfoByMobile(String mobile) {
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
return null;
|
||||
}
|
||||
UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByMobile(mobile);
|
||||
if (userAuthInfo != null) {
|
||||
Set<String> roles = userAuthInfo.getRoles();
|
||||
// 获取数据权限列表(用于并集策略)
|
||||
SecurityUser securityUser = this.baseMapper.getAuthInfoByMobile(mobile);
|
||||
if (securityUser != null) {
|
||||
Set<String> roles = securityUser.getRoles();
|
||||
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
|
||||
userAuthInfo.setDataScopes(dataScopes);
|
||||
securityUser.setDataScopes(dataScopes);
|
||||
}
|
||||
return userAuthInfo;
|
||||
return securityUser;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.youlai.boot.system.service.impl;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.mapper.UserSocialMapper;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
@@ -86,7 +86,7 @@ public class UserSocialServiceImpl extends ServiceImpl<UserSocialMapper, UserSoc
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuthInfo getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) {
|
||||
public SecurityUser getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) {
|
||||
UserSocial userSocial = getByPlatformAndOpenid(platform, openid);
|
||||
if (userSocial == null) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user