refactor(platform):重构平台模块包结构- 将 shared 包下的文件移动到 platform 包下
- 更新相关类的包引用路径 - 修改 application.yml 中的包扫描路径 -重命名 CaptchaInfo 类为 CaptchaVO 并调整包路径 - 移动 BusinessException 和相关安全类到 core 包- 更新 Codegen 相关类包路径 - 删除无用的条件判断代码块
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
46
src/main/java/com/youlai/boot/security/model/OnlineUser.java
Normal file
46
src/main/java/com/youlai/boot/security/model/OnlineUser.java
Normal 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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
106
src/main/java/com/youlai/boot/security/model/SysUserDetails.java
Normal file
106
src/main/java/com/youlai/boot/security/model/SysUserDetails.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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时永不过期
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
126
src/main/java/com/youlai/boot/security/util/SecurityUtils.java
Normal file
126
src/main/java/com/youlai/boot/security/util/SecurityUtils.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user