refactor(platform):重构平台模块包结构- 将 shared 包下的文件移动到 platform 包下

- 更新相关类的包引用路径
- 修改 application.yml 中的包扫描路径
-重命名 CaptchaInfo 类为 CaptchaVO 并调整包路径
- 移动 BusinessException 和相关安全类到 core 包- 更新 Codegen 相关类包路径
- 删除无用的条件判断代码块
This commit is contained in:
Ray.Hao
2025-10-14 16:09:46 +08:00
parent f460d8a7c0
commit c43e6dfb54
122 changed files with 346 additions and 475 deletions

View File

@@ -0,0 +1,15 @@
package com.youlai.boot.core.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 验证码校验异常
*
* @author Ray.Hao
* @since 2025/3/1
*/
public class CaptchaValidationException extends AuthenticationException {
public CaptchaValidationException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,76 @@
package com.youlai.boot.security.filter;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.core.web.ResultCode;
import com.youlai.boot.core.web.WebResponseHelper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 图形验证码校验过滤器
*
* @author haoxr
* @since 2022/10/1
*/
public class CaptchaValidationFilter extends OncePerRequestFilter {
private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name());
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
private final RedisTemplate<String, Object> redisTemplate;
private final CodeGenerator codeGenerator;
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
this.redisTemplate = redisTemplate;
this.codeGenerator = codeGenerator;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 检验登录接口的验证码
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
// 请求中的验证码
String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
// TODO 兼容没有验证码的版本(线上请移除这个判断)
if (StrUtil.isBlank(captchaCode)) {
chain.doFilter(request, response);
return;
}
// 缓存中的验证码
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey)
);
if (cacheVerifyCode == null) {
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
} else {
// 验证码比对
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
chain.doFilter(request, response);
} else {
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
}
}
} else {
// 非登录接口放行
chain.doFilter(request, response);
}
}
}

View File

@@ -0,0 +1,73 @@
package com.youlai.boot.security.filter;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.core.web.ResultCode;
import com.youlai.boot.core.web.WebResponseHelper;
import com.youlai.boot.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.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* Token 认证校验过滤器
*
* @author wangtao
* @since 2025/3/6 16:50
*/
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 {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
try {
if (StrUtil.isNotBlank(authorizationHeader)
&& authorizationHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
// 剥离Bearer前缀获取原始令牌
String rawToken = authorizationHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
// 执行令牌有效性检查(包含密码学验签和过期时间验证)
boolean isValidToken = tokenManager.validateToken(rawToken);
if (!isValidToken) {
WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
return;
}
// 将令牌解析为 Spring Security 上下文认证对象
Authentication authentication = tokenManager.parseToken(rawToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
// 安全上下文清除保障(防止上下文残留)
SecurityContextHolder.clearContext();
WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
return;
}
// 继续后续过滤器链执行
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,26 @@
package com.youlai.boot.security.handler;
import com.youlai.boot.core.web.ResultCode;
import com.youlai.boot.core.web.WebResponseHelper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 无权限访问处理器
*
* @author Ray.Hao
* @since 2.0.0
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
WebResponseHelper.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
}
}

View File

@@ -0,0 +1,48 @@
package com.youlai.boot.security.handler;
import com.youlai.boot.core.web.ResultCode;
import com.youlai.boot.core.web.WebResponseHelper;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 统一处理 Spring Security 认证失败响应
*
* @author Ray.Hao
* @since 2.0.0
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* 认证失败处理入口方法
*
* @param request 触发异常的请求对象(可用于获取请求头、参数等)
* @param response 响应对象(用于写入错误信息)
* @param authException 认证异常对象(包含具体失败原因)
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if (authException instanceof BadCredentialsException) {
// 用户名或密码错误
WebResponseHelper.writeError(response, ResultCode.USER_PASSWORD_ERROR);
} else if(authException instanceof InsufficientAuthenticationException){
// 请求头缺失Authorization、Token格式错误、Token过期、签名验证失败
WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
} else {
// 其他未明确处理的认证异常(如账户被锁定、账户禁用等)
WebResponseHelper.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
}
}
}

View File

@@ -0,0 +1,30 @@
package com.youlai.boot.security.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 认证令牌响应对象
*
* @author Ray.Hao
* @since 0.0.1
*/
@Schema(description = "认证令牌响应对象")
@Data
@Builder
public class AuthenticationToken {
@Schema(description = "令牌类型", example = "Bearer")
private String tokenType;
@Schema(description = "访问令牌")
private String accessToken;
@Schema(description = "刷新令牌")
private String refreshToken;
@Schema(description = "过期时间(单位:秒)")
private Integer expiresIn;
}

View File

@@ -0,0 +1,46 @@
package com.youlai.boot.security.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
/**
* 在线用户信息对象
*
* @author wangtao
* @since 2025/2/27 10:31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OnlineUser {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 部门ID
*/
private Long deptId;
/**
* 数据权限范围
* <p>定义用户可访问的数据范围,如全部、本部门或自定义范围</p>
*/
private Integer dataScope;
/**
* 角色权限集合
*/
private Set<String> roles;
}

View File

@@ -0,0 +1,78 @@
package com.youlai.boot.security.model;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serial;
import java.util.Collection;
/**
* 短信验证码认证 Token
*
* @author Ray.Hao
* @since 2.20.0
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 621L;
/**
* 认证信息 (手机号)
*/
private final Object principal;
/**
* 凭证信息 (短信验证码)
*/
private final Object credentials;
/**
* 短信验证码认证 Token (未认证)
*
* @param principal 微信用户信息
*/
public SmsAuthenticationToken(Object principal, Object credentials) {
// 没有授权信息时,设置为 null
super(null);
this.principal = principal;
this.credentials = credentials;
// 默认未认证
this.setAuthenticated(false);
}
/**
* 短信验证码认证 Token (已认证)
*
* @param principal 用户信息
* @param authorities 授权信息
*/
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
// 认证通过
super.setAuthenticated(true);
}
/**
* 认证通过
*
* @param principal 用户信息
* @param authorities 授权信息
* @return SmsAuthenticationToken
*/
public static SmsAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
return new SmsAuthenticationToken(principal, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}

View File

@@ -0,0 +1,106 @@
package com.youlai.boot.security.model;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
/**
* Spring Security 用户认证对象
* <p>
* 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。
* 实现了 {@link UserDetails} 接口,提供用户的核心信息。
*
* @author Ray.Hao
* @version 3.0.0
*/
@Data
@NoArgsConstructor
public class SysUserDetails implements UserDetails {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 账号是否启用(true:启用 false:禁用)
*/
private Boolean enabled;
/**
* 部门ID
*/
private Long deptId;
/**
* 数据权限范围
*/
private Integer dataScope;
/**
* 用户角色权限集合
*/
private Collection<SimpleGrantedAuthority> authorities;
/**
* 构造函数:根据用户认证信息初始化用户详情对象
*
* @param user 用户认证信息对象 {@link UserAuthCredentials}
*/
public SysUserDetails(UserAuthCredentials 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.dataScope = user.getDataScope();
// 初始化角色权限集合
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();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}

View File

@@ -0,0 +1,57 @@
package com.youlai.boot.security.model;
import lombok.Data;
import java.util.Set;
/**
* 用户认证凭证信息
*
* @author Ray.Hao
* @since 2022/10/22
*/
@Data
public class UserAuthCredentials {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickname;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户密码
*/
private String password;
/**
* 状态1:启用0:禁用)
*/
private Integer status;
/**
* 用户所属的角色集合
*/
private Set<String> roles;
/**
* 数据权限范围,用于控制用户可以访问的数据级别
*
* @see com.youlai.boot.common.enums.DataScopeEnum
*/
private Integer dataScope;
}

View File

@@ -0,0 +1,69 @@
package com.youlai.boot.security.model;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serial;
import java.util.Collection;
/**
* 微信小程序Code认证Token
*
* @author 有来技术团队
* @since 2.0.0
*/
public class WxMiniAppCodeAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 621L;
private final Object principal;
/**
* 微信小程序Code认证Token (未认证)
*
* @param principal 微信code
*/
public WxMiniAppCodeAuthenticationToken(Object principal) {
// 没有授权信息时,设置为 null
super(null);
this.principal = principal;
// 默认未认证
this.setAuthenticated(false);
}
/**
* 微信小程序Code认证Token (已认证)
*
* @param principal 微信用户信息
* @param authorities 授权信息
*/
public WxMiniAppCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// 认证通过
super.setAuthenticated(true);
}
/**
* 认证通过
*
* @param principal 微信用户信息
* @param authorities 授权信息
* @return 已认证的Token
*/
public static WxMiniAppCodeAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
return new WxMiniAppCodeAuthenticationToken(principal, authorities);
}
@Override
public Object getCredentials() {
// 微信认证不需要密码
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}

View File

@@ -0,0 +1,89 @@
package com.youlai.boot.security.model;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.io.Serial;
import java.util.Collection;
/**
* 微信小程序手机号认证Token
*
* @author 有来技术团队
* @since 2.0.0
*/
public class WxMiniAppPhoneAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 622L;
private final Object principal; // code
private String encryptedData;
private String iv;
/**
* 微信小程序手机号认证Token (未认证)
*
* @param code 微信登录code
* @param encryptedData 加密数据
* @param iv 初始向量
*/
public WxMiniAppPhoneAuthenticationToken(String code, String encryptedData, String iv) {
super(null);
this.principal = code;
this.encryptedData = encryptedData;
this.iv = iv;
this.setAuthenticated(false);
}
/**
* 微信小程序手机号认证Token (已认证)
*
* @param principal 用户信息
* @param authorities 授权信息
*/
public WxMiniAppPhoneAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
/**
* 认证通过
*
* @param principal 用户信息
* @param authorities 授权信息
* @return 认证通过的Token
*/
public static WxMiniAppPhoneAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
return new WxMiniAppPhoneAuthenticationToken(principal, authorities);
}
@Override
public Object getCredentials() {
// 微信小程序手机号认证不需要密码
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
/**
* 获取加密数据
*
* @return 加密数据
*/
public String getEncryptedData() {
return encryptedData;
}
/**
* 获取初始向量
*
* @return 初始向量
*/
public String getIv() {
return iv;
}
}

View File

@@ -0,0 +1,89 @@
package com.youlai.boot.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.core.exception.CaptchaValidationException;
import com.youlai.boot.security.model.SmsAuthenticationToken;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.model.UserAuthCredentials;
import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 短信验证码认证 Provider
*
* @author Ray.Hao
* @since 2.17.0
*/
@Slf4j
public class SmsAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final RedisTemplate<String, Object> redisTemplate;
public SmsAuthenticationProvider(UserService userService, RedisTemplate<String, Object> redisTemplate) {
this.userService = userService;
this.redisTemplate = redisTemplate;
}
/**
* 短信验证码认证逻辑,参考 Spring Security 认证密码校验流程
*
* @param authentication 认证对象
* @return 认证后的 Authentication 对象
* @throws AuthenticationException 认证异常
* @see org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate(Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = (String) authentication.getPrincipal();
String inputVerifyCode = (String) authentication.getCredentials();
// 根据手机号获取用户信息
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(mobile);
if (userAuthCredentials == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 检查用户状态是否有效
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
// 校验发送短信验证码的手机号是否与当前登录用户一致
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
String cachedVerifyCode = (String) redisTemplate.opsForValue().get(cacheKey);
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
throw new CaptchaValidationException("验证码错误");
} else {
// 验证成功后删除验证码
redisTemplate.delete(cacheKey);
}
// 构建认证后的用户详情信息
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
// 创建已认证的 SmsAuthenticationToken
return SmsAuthenticationToken.authenticated(
userDetails,
userDetails.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -0,0 +1,98 @@
package com.youlai.boot.security.provider;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.model.UserAuthCredentials;
import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken;
import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 微信小程序Code认证Provider
*
* @author 有来技术团队
* @since 2.0.0
*/
@Slf4j
public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final WxMaService wxMaService;
public WxMiniAppCodeAuthenticationProvider(UserService userService, WxMaService wxMaService) {
this.userService = userService;
this.wxMaService = wxMaService;
}
/**
* 微信认证逻辑,参考 Spring Security 认证密码校验流程
*
* @param authentication 认证对象
* @return 认证后的 Authentication 对象
* @throws AuthenticationException 认证异常
* @see org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate(Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String code = (String) authentication.getPrincipal();
// 通过微信服务端验证 code 并获取用户会话信息
WxMaJscode2SessionResult sessionInfo;
try {
sessionInfo = wxMaService.getUserService().getSessionInfo(code);
} catch (WxErrorException e) {
throw new CredentialsExpiredException("微信登录 code 无效或已失效,请重新获取");
}
String openId = sessionInfo.getOpenid();
if (StrUtil.isBlank(openId)) {
throw new UsernameNotFoundException("未能获取到微信 OpenID请稍后重试");
}
// 根据微信 OpenID 查询用户信息
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
if (userAuthCredentials == null) {
// 用户不存在则注册
userService.registerOrBindWechatUser(openId);
// 再次查询用户信息,确保用户注册成功
userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
if (userAuthCredentials == null) {
throw new UsernameNotFoundException("用户注册失败,请稍后重试");
}
}
// 检查用户状态是否有效
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
// 构建认证后的用户详情信息
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
// 创建已认证的Token
return WxMiniAppCodeAuthenticationToken.authenticated(
userDetails,
userDetails.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return WxMiniAppCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -0,0 +1,115 @@
package com.youlai.boot.security.provider;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.model.UserAuthCredentials;
import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken;
import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 微信小程序手机号认证Provider
*
* @author 有来技术团队
* @since 2.0.0
*/
@Slf4j
public class WxMiniAppPhoneAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final WxMaService wxMaService;
public WxMiniAppPhoneAuthenticationProvider(UserService userService, WxMaService wxMaService) {
this.userService = userService;
this.wxMaService = wxMaService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
WxMiniAppPhoneAuthenticationToken authenticationToken = (WxMiniAppPhoneAuthenticationToken) authentication;
String code = (String) authenticationToken.getPrincipal();
String encryptedData = authenticationToken.getEncryptedData();
String iv = authenticationToken.getIv();
// 1. 通过code获取session_key
WxMaJscode2SessionResult sessionInfo;
try {
sessionInfo = wxMaService.getUserService().getSessionInfo(code);
} catch (WxErrorException e) {
log.error("获取微信session_key失败", e);
throw new CredentialsExpiredException("微信登录code无效或已过期");
}
String sessionKey = sessionInfo.getSessionKey();
String openId = sessionInfo.getOpenid();
if (StrUtil.isBlank(sessionKey) || StrUtil.isBlank(openId)) {
throw new CredentialsExpiredException("获取微信会话信息失败");
}
// 2. 解密手机号信息
WxMaPhoneNumberInfo phoneNumberInfo;
try {
if (StrUtil.isNotBlank(encryptedData) && StrUtil.isNotBlank(iv)) {
phoneNumberInfo = wxMaService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv);
} else {
throw new IllegalArgumentException("缺少手机号加密数据");
}
} catch (Exception e) {
log.error("解密微信手机号失败", e);
throw new CredentialsExpiredException("解密手机号信息失败");
}
if (phoneNumberInfo == null || StrUtil.isBlank(phoneNumberInfo.getPhoneNumber())) {
throw new CredentialsExpiredException("获取手机号失败");
}
String phoneNumber = phoneNumberInfo.getPhoneNumber();
// 3. 根据手机号查询用户,不存在则创建新用户
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber);
if (userAuthCredentials == null) {
// 用户不存在,注册新用户
boolean registered = userService.registerUserByMobileAndOpenId(phoneNumber, openId);
if (!registered) {
throw new UsernameNotFoundException("用户注册失败");
}
// 重新获取用户信息
userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber);
} else {
// 用户存在绑定openId如果未绑定
userService.bindUserOpenId(userAuthCredentials.getUserId(), openId);
}
// 4. 检查用户状态
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
// 5. 构建认证后的用户详情
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
// 6. 创建已认证的Token
return WxMiniAppPhoneAuthenticationToken.authenticated(
userDetails,
userDetails.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return WxMiniAppPhoneAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -0,0 +1,97 @@
package com.youlai.boot.security.service;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.PatternMatchUtils;
import java.util.*;
/**
* SpringSecurity 权限校验
*
* @author haoxr
* @since 2022/2/22
*/
@Component("ss")
@RequiredArgsConstructor
@Slf4j
public class PermissionService {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 判断当前登录用户是否拥有操作权限
*
* @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 = this.getRolePermsFormCache(roleCodes);
if (CollectionUtil.isEmpty(rolePerms)) {
return false;
}
// 判断当前登录用户的所有角色的权限列表中是否包含所需权限
boolean hasPermission = rolePerms.stream()
.anyMatch(rolePerm ->
// 匹配权限,支持通配符(* 等)
PatternMatchUtils.simpleMatch(rolePerm, requiredPerm)
);
if (!hasPermission) {
log.error("用户无操作权限:{}",requiredPerm);
}
return hasPermission;
}
/**
* 从缓存中获取角色权限列表
*
* @param roleCodes 角色编码集合
* @return 角色权限列表
*/
public Set<String> getRolePermsFormCache(Set<String> roleCodes) {
// 检查输入是否为空
if (CollectionUtil.isEmpty(roleCodes)) {
return Collections.emptySet();
}
Set<String> perms = new HashSet<>();
// 从缓存中一次性获取所有角色的权限
Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects);
for (Object rolePermsObj : rolePermsList) {
if (rolePermsObj instanceof Set) {
@SuppressWarnings("unchecked")
Set<String> rolePerms = (Set<String>) rolePermsObj;
perms.addAll(rolePerms);
}
}
return perms;
}
}

View File

@@ -0,0 +1,48 @@
package com.youlai.boot.security.service;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.model.UserAuthCredentials;
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;
/**
* 根据用户名获取用户信息
*
* @param username 用户名
* @return 用户信息
* @throws UsernameNotFoundException 用户名未找到异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByUsername(username);
if (userAuthCredentials == null) {
throw new UsernameNotFoundException(username);
}
return new SysUserDetails(userAuthCredentials);
} catch (Exception e) {
// 记录异常日志
log.error("认证异常:{}", e.getMessage());
// 抛出异常
throw e;
}
}
}

View File

@@ -0,0 +1,273 @@
package com.youlai.boot.security.token;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import com.youlai.boot.common.constant.JwtClaimConstants;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.core.web.ResultCode;
import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.security.model.AuthenticationToken;
import org.apache.commons.lang3.StringUtils;
import com.youlai.boot.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.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* JWT Token 管理器
* <p>
* 用于生成、解析、校验、刷新 JWT Token
*
* @author Ray.Hao
* @since 2024/11/15
*/
@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
@Service
public class JwtTokenManager implements TokenManager {
private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate;
private final byte[] secretKey;
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes();
}
/**
* 生成令牌
*
* @param authentication 认证信息
* @return 令牌响应对象
*/
@Override
public AuthenticationToken generateToken(Authentication authentication) {
int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive();
String accessToken = generateToken(authentication, accessTokenTimeToLive);
String refreshToken = generateToken(authentication, refreshTokenTimeToLive, true);
return AuthenticationToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(accessTokenTimeToLive)
.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
userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
// 角色集合
Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES)
.stream()
.map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
.collect(Collectors.toSet());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
/**
* 校验令牌
*
* @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);
}
/**
* 校验令牌
*
* @param token JWT Token
* @param validateRefreshToken 是否校验刷新令牌
* @return 是否有效
*/
private boolean validateToken(String token, boolean validateRefreshToken) {
try {
JWT jwt = JWTUtil.parseToken(token);
// 检查 Token 是否有效(验签 + 是否过期)
boolean isValid = jwt.setKey(secretKey).validate(0);
if (isValid) {
// 检查 Token 是否已被加入黑名单(注销、修改密码等场景)
JSONObject payloads = jwt.getPayloads();
String jti = payloads.getStr(JWTPayload.JWT_ID);
if(validateRefreshToken) {
//刷新token需要校验token类别
boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
if (!isRefreshToken) {
return false;
}
}
// 判断是否在黑名单中,如果在,则返回 false 标识Token无效
if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
return false;
}
}
return isValid;
} catch (Exception gitignore) {
// token 验证
}
return false;
}
/**
* 将令牌加入黑名单
*
* @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());
}
JWT jwt = JWTUtil.parseToken(token);
JSONObject payloads = jwt.getPayloads();
Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT);
// 黑名单Token Key
String blacklistTokenKey = StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, payloads.getStr(JWTPayload.JWT_ID));
if (expirationAt != null) {
int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000);
if (expirationAt < currentTimeSeconds) {
// Token已过期直接返回
return;
}
// 计算Token剩余时间将其加入黑名单
int expirationIn = expirationAt - currentTimeSeconds;
redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS);
} else {
// 永不过期的Token永久加入黑名单
redisTemplate.opsForValue().set(blacklistTokenKey, null);
}
;
}
/**
* 刷新令牌
*
* @param refreshToken 刷新令牌
* @return 令牌响应对象
*/
@Override
public AuthenticationToken refreshToken(String refreshToken) {
boolean isValid = validateRefreshToken(refreshToken);
if (!isValid) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
}
Authentication authentication = parseToken(refreshToken);
int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive();
String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthenticationToken.builder()
.accessToken(newAccessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(accessTokenExpiration)
.build();
}
/**
* 生成 JWT Token
*
* @param authentication 认证信息
* @param ttl 过期时间
* @return JWT Token
*/
private String generateToken(Authentication authentication, int ttl) {
return generateToken(authentication, ttl, false);
}
/**
* 生成 JWT Token
*
* @param authentication 认证信息
* @param ttl 过期时间
* @param isRefreshToken 类型是否为刷新token
* @return JWT Token
*/
private String generateToken(Authentication authentication, int ttl, boolean isRefreshToken) {
SysUserDetails userDetails = (SysUserDetails) 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.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围
// claims 中添加角色信息
Set<String> roles = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
payload.put(JwtClaimConstants.AUTHORITIES, roles);
Date now = new Date();
payload.put(JWTPayload.ISSUED_AT, now);
payload.put(JwtClaimConstants.TOKEN_TYPE, false);
if (isRefreshToken) {
payload.put(JwtClaimConstants.TOKEN_TYPE, true);
}
// 设置过期时间 -1 表示永不过期
if (ttl != -1) {
Date expiresAt = DateUtil.offsetSecond(now, ttl);
payload.put(JWTPayload.EXPIRES_AT, expiresAt);
}
payload.put(JWTPayload.SUBJECT, authentication.getName());
payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());
return JWTUtil.createToken(payload, secretKey);
}
}

View File

@@ -0,0 +1,293 @@
package com.youlai.boot.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;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.core.web.ResultCode;
import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.security.model.AuthenticationToken;
import com.youlai.boot.security.model.OnlineUser;
import com.youlai.boot.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.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Redis Token 管理器
* <p>
* 用于生成、解析、校验、刷新 Redis Token
*
* @author Ray.Hao
* @since 2024/11/15
*/
@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token")
@Service
public class RedisTokenManager implements TokenManager {
private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate;
public RedisTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
}
/**
* 生成 Token
*
* @param authentication 用户认证信息
* @return 生成的 AuthenticationToken 对象
*/
@Override
public AuthenticationToken generateToken(Authentication authentication) {
SysUserDetails user = (SysUserDetails) authentication.getPrincipal();
String accessToken = IdUtil.fastSimpleUUID();
String refreshToken = IdUtil.fastSimpleUUID();
// 构建用户在线信息
OnlineUser onlineUser = new OnlineUser(
user.getUserId(),
user.getUsername(),
user.getDeptId(),
user.getDataScope(),
user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet())
);
// 存储访问令牌、刷新令牌和刷新令牌映射
storeTokensInRedis(accessToken, refreshToken, onlineUser);
// 单设备登录控制
handleSingleDeviceLogin(user.getUserId(), accessToken);
return AuthenticationToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(securityProperties.getSession().getAccessTokenTimeToLive())
.build();
}
/**
* 根据 token 解析用户信息
*
* @param token Redis Token
* @return 构建的 Authentication 对象
*/
@Override
public Authentication parseToken(String token) {
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
if (onlineUser == null) return null;
// 构建用户权限集合
Set<SimpleGrantedAuthority> authorities = null;
Set<String> roles = onlineUser.getRoles();
if (CollectionUtil.isNotEmpty(roles)) {
authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}
// 构建用户详情对象
SysUserDetails userDetails = buildUserDetails(onlineUser, authorities);
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
}
/**
* 校验 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) {
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
if (onlineUser == null) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
}
String oldAccessToken = (String) redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()));
// 删除旧的访问令牌记录
if (oldAccessToken != null) {
redisTemplate.delete(formatTokenKey(oldAccessToken));
}
// 生成新访问令牌并存储
String newAccessToken = IdUtil.fastSimpleUUID();
storeAccessToken(newAccessToken, onlineUser);
int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
return AuthenticationToken.builder()
.accessToken(newAccessToken)
.refreshToken(refreshToken)
.expiresIn(accessTtl)
.build();
}
/**
* 使访问令牌失效
*
* @param token 访问令牌
*/
@Override
public void invalidateToken(String token) {
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
if (onlineUser != null) {
Long userId = onlineUser.getUserId();
// 1. 删除访问令牌相关
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
String accessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
if (accessToken != null) {
redisTemplate.delete(formatTokenKey(accessToken));
redisTemplate.delete(userAccessKey);
}
// 2. 删除刷新令牌相关
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey);
if (refreshToken != null) {
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
redisTemplate.delete(userRefreshKey);
}
}
}
/**
* 将访问令牌和刷新令牌存储至 Redis
*
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param onlineUser 在线用户信息
*/
private void storeTokensInRedis(String accessToken, String refreshToken, OnlineUser onlineUser) {
// 访问令牌 -> 用户信息
setRedisValue(formatTokenKey(accessToken), onlineUser, securityProperties.getSession().getAccessTokenTimeToLive());
// 刷新令牌 -> 用户信息
String refreshTokenKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
setRedisValue(refreshTokenKey, onlineUser, securityProperties.getSession().getRefreshTokenTimeToLive());
// 用户ID -> 刷新令牌
setRedisValue(StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, onlineUser.getUserId()),
refreshToken,
securityProperties.getSession().getRefreshTokenTimeToLive());
}
/**
* 处理单设备登录控制
*
* @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) {
String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
if (oldAccessToken != null) {
redisTemplate.delete(formatTokenKey(oldAccessToken));
}
}
// 存储访问令牌映射用户ID -> 访问令牌),用于单设备登录控制删除旧的访问令牌和刷新令牌时删除旧令牌
setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
}
/**
* 存储新的访问令牌
*
* @param newAccessToken 新访问令牌
* @param onlineUser 在线用户信息
*/
private void storeAccessToken(String newAccessToken, OnlineUser onlineUser) {
setRedisValue(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), onlineUser, securityProperties.getSession().getAccessTokenTimeToLive());
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId());
setRedisValue(userAccessKey, newAccessToken, securityProperties.getSession().getAccessTokenTimeToLive());
}
/**
* 构建用户详情对象
*
* @param onlineUser 在线用户信息
* @param authorities 权限集合
* @return SysUserDetails 用户详情
*/
private SysUserDetails buildUserDetails(OnlineUser onlineUser, Set<SimpleGrantedAuthority> authorities) {
SysUserDetails userDetails = new SysUserDetails();
userDetails.setUserId(onlineUser.getUserId());
userDetails.setUsername(onlineUser.getUsername());
userDetails.setDeptId(onlineUser.getDeptId());
userDetails.setDataScope(onlineUser.getDataScope());
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时永不过期
}
}
}

View File

@@ -0,0 +1,68 @@
package com.youlai.boot.security.token;
import com.youlai.boot.security.model.AuthenticationToken;
import org.springframework.security.core.Authentication;
/**
* Token 管理器
* <p>
* 用于生成、解析、校验、刷新 Token
*
* @author Ray.Hao
* @since 2.16.0
*/
public interface TokenManager {
/**
* 生成认证 Token
*
* @param authentication 用户认证信息
* @return 认证 Token 响应
*/
AuthenticationToken generateToken(Authentication authentication);
/**
* 解析 Token 获取认证信息
*
* @param token Token
* @return 用户认证信息
*/
Authentication parseToken(String token);
/**
* 校验 Token 是否有效
*
* @param token JWT Token
* @return 是否有效
*/
boolean validateToken(String token);
/**
* 校验 刷新 Token 是否有效
*
* @param refreshToken JWT Token
* @return 是否有效
*/
boolean validateRefreshToken(String refreshToken);
/**
* 刷新 Token
*
* @param token 刷新令牌
* @return 认证 Token 响应
*/
AuthenticationToken refreshToken(String token);
/**
* 令 Token 失效
*
* @param token Token
*/
default void invalidateToken(String token) {
// 默认实现可以是空的,或者抛出不支持的操作异常
// throw new UnsupportedOperationException("Not implemented");
}
}

View File

@@ -0,0 +1,126 @@
package com.youlai.boot.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.security.model.SysUserDetails;
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 工具类
*
* @author Ray
* @since 2021/1/10
*/
public class SecurityUtils {
/**
* 获取当前登录人信息
*
* @return Optional<SysUserDetails>
*/
public static Optional<SysUserDetails> getUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal instanceof SysUserDetails) {
return Optional.of((SysUserDetails) principal);
}
}
return Optional.empty();
}
/**
* 获取用户ID
*
* @return Long
*/
public static Long getUserId() {
return getUser().map(SysUserDetails::getUserId).orElse(null);
}
/**
* 获取用户账号
*
* @return String 用户账号
*/
public static String getUsername() {
return getUser().map(SysUserDetails::getUsername).orElse(null);
}
/**
* 获取部门ID
*
* @return Long
*/
public static Long getDeptId() {
return getUser().map(SysUserDetails::getDeptId).orElse(null);
}
/**
* 获取数据权限范围
*
* @return Integer
*/
public static Integer getDataScope() {
return getUser().map(SysUserDetails::getDataScope).orElse(null);
}
/**
* 获取角色集合
*
* @return 角色集合
*/
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());
}
/**
* 是否超级管理员
* <p>
* 超级管理员忽视任何权限判断
*/
public static boolean isRoot() {
Set<String> roles = getRoles();
return roles.contains(SystemConstants.ROOT_ROLE_CODE);
}
/**
* 获取请求中的 Token
*
* @return Token 字符串
*/
public static String getTokenFromRequest() {
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if(Objects.isNull(servletRequestAttributes)) {
return null;
}
HttpServletRequest request = servletRequestAttributes.getRequest();
return request.getHeader(HttpHeaders.AUTHORIZATION);
}
}