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,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");
}
}