refactor: 优化 redis-token 会话管理和单一会话控制

This commit is contained in:
Ray.Hao
2025-03-08 14:11:43 +08:00
parent e01b784a97
commit d3f5ba25ba
14 changed files with 353 additions and 375 deletions

View File

@@ -26,10 +26,16 @@ public interface RedisConstants {
* 认证模块
*/
interface Auth {
String ACCESS_TOKEN = "auth:token:access:{}"; // 访问Token
String REFRESH_TOKEN = "auth:token:refresh:{}"; // 刷新Token
String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; // 黑名单Token
// 存储访问令牌对应的用户信息accessToken -> OnlineUser
String ACCESS_TOKEN_USER = "auth:token:access:{}";
// 存储刷新令牌对应的用户信息refreshToken -> OnlineUser
String REFRESH_TOKEN_USER = "auth:token:refresh:{}";
// 用户与访问令牌的映射userId -> accessToken
String USER_ACCESS_TOKEN = "auth:user:access:{}";
// 用户与刷新令牌的映射userId -> refreshToken
String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
// 黑名单 Token用于退出登录或注销
String BLACKLIST_TOKEN = "auth:token:blacklist:{}";
}
/**

View File

@@ -1,20 +0,0 @@
package com.youlai.boot.common.enums;
import lombok.Getter;
/**
* @Description TODO
* @Author wangtao
* @Date 2025/2/27 14:48
*/
@Getter
public enum TokenKeyEnum {
ACCESS_TOKEN_KEY("access_token:"),
REFRESH_TOKEN_KEY ("refresh_token:");
private final String value;
TokenKeyEnum(String value) {
this.value = value;
}
}

View File

@@ -11,7 +11,7 @@ import com.youlai.boot.core.security.extension.sms.SmsAuthenticationProvider;
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationProvider;
import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
import com.youlai.boot.core.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.core.security.service.SysUserDetailsService;
import com.youlai.boot.system.service.ConfigService;
import com.youlai.boot.system.service.UserService;

View File

@@ -1,108 +1,111 @@
package com.youlai.boot.config.property;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.List;
/**
* 安全配置属性
* 安全模块配置属性
*
* <p>映射 application.yml 中 security 前缀的安全相关配置</p>
*
* @author Ray.Hao
* @since 2024/4/18
*/
@Data
@Component
@Validated
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
/**
* 免认证请求路径白名单
* 会话管理配置
*/
private List<String> ignoreUrls = new ArrayList<>();
private SessionConfig session;
/**
* 静态资源路径(不经过安全过滤器)
* 安全白名单路径(完全绕过安全过滤器)
* <p>示例值:/api/v1/auth/login/**, /ws/**
*/
private List<String> unsecuredUrls = new ArrayList<>();
@NotEmpty
private String[] ignoreUrls;
/**
* 认证核心配置
* 非安全端点路径允许匿名访问的API
* <p>示例值:/doc.html, /v3/api-docs/**
*/
private Auth auth = new Auth();
@NotEmpty
private String[] unsecuredUrls;
/**
* 会话配置嵌套类
*/
@Data
public static class Auth {
public static class SessionConfig {
/**
* 认证策略类型
* <ul>
* <li>jwt - 基于JWT的无状态认证</li>
* <li>redis-token - 基于Redis的有状态认证</li>
* </ul>
*/
@NotNull
private AuthType type = AuthType.JWT;
private String type;
/**
* 访问令牌有效期(秒)
* 访问令牌有效期(单位:秒)
* <p>默认值36001小时</p>
* <p>-1 表示永不过期</p>
*/
@Min(-1)
private int accessTokenTtl = 3600;
private Integer accessTokenTimeToLive = 3600;
/**
* 刷新令牌有效期(秒)
* 刷新令牌有效期(单位:秒)
* <p>默认值6048007天</p>
* <p>-1 表示永不过期</p>
*/
@Min(-1)
private int refreshTokenTtl = 604800;
private Integer refreshTokenTimeToLive = 604800;
/**
* JWT 配置
* JWT 配置
*/
private JwtConfig jwtConfig = new JwtConfig();
private JwtConfig jwt;
/**
* Redis Token 配置
* Redis令牌配置
*/
private RedisTokenConfig redisTokenConfig = new RedisTokenConfig();
@Data
public static class JwtConfig {
/**
* JWT 密钥
*/
@NotBlank
@Size(min = 32, message = "HS256算法密钥至少需要32字符")
private String key;
}
@Data
public static class RedisTokenConfig {
/**
* 最大并发会话数
*/
@Min(-1)
private int maxSessions = 1;
/**
* 会话超限处理策略
*/
private SessionControlStrategy sessionControl = SessionControlStrategy.REVOKE_OLDEST;
}
private RedisTokenConfig redisToken;
}
/**
* 认证策略类型枚举
* JWT 配置嵌套类
*/
public enum AuthType {
JWT, REDIS_TOKEN
@Data
public static class JwtConfig {
/**
* JWT签名密钥
* <p>HS256算法要求至少32个字符</p>
* <p>示例SecretKey012345678901234567890123456789</p>
*/
@NotNull
private String secretKey;
}
/**
* 会话控制策略枚举
* Redis令牌配置嵌套类
*/
public enum SessionControlStrategy {
REVOKE_OLDEST, DENY_NEW
@Data
public static class RedisTokenConfig {
/**
* 是否允许多设备同时登录
* <p>true - 允许同一账户多设备登录(默认)</p>
* <p>false - 新登录会使旧令牌失效</p>
*/
private Boolean allowMultiLogin = true;
}
}

View File

@@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.ResponseUtils;
import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.core.security.token.TokenManager;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;

View File

@@ -1,255 +0,0 @@
package com.youlai.boot.core.security.manager;
import cn.hutool.core.convert.Convert;
import com.youlai.boot.common.enums.TokenKeyEnum;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.core.security.model.OnlineUser;
import com.youlai.boot.core.security.model.SysUserDetails;
import org.apache.commons.lang3.StringUtils;
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.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Redis Token 管理器
* <p>
* 用于生成、解析、校验、刷新 JWT Token
*
* @author Ray.Hao
* @since 2024/11/15
*/
@ConditionalOnProperty(value = "security.auth.type", havingValue = "redis-token")
@Service
public class RedisTokenManager implements TokenManager {
// 常量定义
private static final String USER_SESSION_MAP = "user_sessions:%s"; // %s=token_type
private static final String SESSION_KEY = "session:%s:%s"; // %s=token_type,token
private static final String SESSION_QUEUE = "session_queue:%s"; // %s=user_id
private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate;
public RedisTokenManager(
SecurityProperties securityProperties,
RedisTemplate<String, Object> redisTemplate
) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
}
/**
* 生成令牌
*
* @param authentication 用户认证信息
* @return
*/
@Override
public AuthenticationToken generateToken(Authentication authentication) {
int accessTokenTtl = securityProperties.getAuth().getAccessTokenTtl();
// 创建新会话
String accessToken = createNewSession(authentication, );
// 创建刷新令牌(独立控制)
String refreshToken = createNewSession(authentication, TokenType.REFRESH,
config.getRefreshTokenTtl(), 1); // 刷新令牌强制单设备
return AuthenticationToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(accessTokenTtl)
.build();
}
private String createNewSession(Authentication authentication,
TokenType tokenType,
int ttl,
int maxSessions) {
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
Long userId = userDetails.getUserId();
String token = UUID.randomUUID().toString();
// 会话存储
String sessionKey = keyGenerator.getSessionKey(tokenType, token);
redisTemplate.opsForValue().set(sessionKey, buildOnlineUser(userDetails), ttl, TimeUnit.SECONDS);
// 用户-会话映射
String userMapKey = keyGenerator.getUserSessionMapKey(tokenType);
redisTemplate.opsForHash().put(userMapKey, userId.toString(), token);
redisTemplate.expire(userMapKey, ttl, TimeUnit.SECONDS);
// 多设备控制
enforceMaxSessions(userId, token, tokenType, maxSessions);
return token;
}
private void enforceMaxSessions(Long userId, String currentToken, TokenType tokenType, int maxSessions) {
if (maxSessions <= 0) return;
String sessionQueueKey = keyGenerator.getSessionQueueKey(userId);
long now = System.currentTimeMillis();
// 使用ZSet维护会话队列
redisTemplate.opsForZSet().add(sessionQueueKey, currentToken, now);
redisTemplate.expire(sessionQueueKey, 7, TimeUnit.DAYS);
// 移除超出数量的旧会话
long excess = redisTemplate.opsForZSet().size(sessionQueueKey) - maxSessions;
if (excess > 0) {
Set<String> oldTokens = redisTemplate.opsForZSet().range(sessionQueueKey, 0, excess - 1);
redisTemplate.opsForZSet().removeRange(sessionQueueKey, 0, excess - 1);
// 吊销旧令牌
oldTokens.forEach(oldToken -> {
redisTemplate.delete(keyGenerator.getSessionKey(tokenType, oldToken));
redisTemplate.opsForHash().delete(
keyGenerator.getUserSessionMapKey(tokenType),
userId.toString()
);
});
}
}
/**
* 解析令牌
*
* @param token JWT Token
* @return
*/
@Override
public Authentication parseToken(String token) {
String accessTokenKey = TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token;
OnlineUser user = (OnlineUser) redisTemplate.opsForValue().get(accessTokenKey);
Set<SimpleGrantedAuthority> authorities = user.getAuthorities()
.stream()
.map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
.collect(Collectors.toSet());
SysUserDetails userDetails = new SysUserDetails();
userDetails.setUserId(user.getId());
userDetails.setUsername(user.getUsername());
userDetails.setDeptId(user.getDeptId());
userDetails.setDataScope(user.getDataScope());
userDetails.setAuthorities(authorities);
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
/**
* 验证令牌
*
* @param token JWT Token
* @return
*/
@Override
public boolean validateToken(String token) {
String accessTokenKey = TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token;
return redisTemplate.hasKey(accessTokenKey);
}
/**
* 刷新令牌
*
* @param token 刷新令牌
* @return
*/
@Override
public AuthenticationToken refreshToken(String token) {
String refreshTokenKey = TokenKeyEnum.REFRESH_TOKEN_KEY.getValue() + token;
Authentication authentication = (Authentication) redisTemplate.opsForValue().get(refreshTokenKey);
if (authentication == null) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
}
int accessTokenExpiration = securityProperties.getAuth().getRefreshTokenTtl();
// 生成新的访问令牌
String newAccessToken = generateToken(authentication, TokenKeyEnum.ACCESS_TOKEN_KEY, accessTokenExpiration, true);
return AuthenticationToken.builder()
.accessToken(newAccessToken)
.refreshToken(token)
.expiresIn(accessTokenExpiration)
.build();
}
/**
* 创建令牌
*
* @param authentication 认证信息
* @param tokenKeyEnum 令牌类型
* @param ttl 有效期
* @param multiLogin 是否允许多点登录
* @return
*/
private String generateToken(Authentication authentication, TokenKeyEnum tokenKeyEnum, int ttl, boolean multiLogin) {
// 获取用户信息
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
String token = UUID.randomUUID().toString();
String tokenKey = tokenKeyEnum.getValue() + token;
// 不允许多点登录使用hashmap存储在线用户id和token
if (!multiLogin) {
// 查找当前用户id是否有token有的话说明已经登录了就删除旧的token
String oldToken = (String) redisTemplate.opsForHash().get("userId-token:" + tokenKeyEnum.getValue(), userDetails.getUserId().toString());
if (StringUtils.isNotBlank(oldToken)) {
redisTemplate.opsForHash().delete("userId-token:" + tokenKeyEnum.getValue(), userDetails.getUserId().toString());
redisTemplate.delete(tokenKeyEnum.getValue() + oldToken);
}
redisTemplate.opsForHash().put("userId-token:" + tokenKeyEnum.getValue(), userDetails.getUserId().toString(), token);
// 设置userId-token的过期时间
redisTemplate.opsForHash().getOperations().expire("userId-token:" + tokenKeyEnum.getValue(), ttl, TimeUnit.SECONDS);
}
// 存储用户信息
OnlineUser user = new OnlineUser();
user.setId(userDetails.getUserId());
user.setUsername(userDetails.getUsername());
user.setDeptId(userDetails.getDeptId());
user.setDataScope(userDetails.getDataScope());
// claims 中添加角色信息
Set<String> roles = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
user.setAuthorities(roles);
redisTemplate.opsForValue().set(tokenKey, user, ttl, TimeUnit.SECONDS);
return token;
}
/**
* 清除redis中的用户信息
*
* @param token Redis Token
*/
@Override
public void blacklistToken(String token) {
/**
* 根据token删除当前用户的accessToken和refreshToken以及 userId-token 的hashmap
*/
OnlineUser user = (OnlineUser) redisTemplate.opsForValue().get(TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token);
if (!Objects.isNull(user)) {
Long userId = user.getId();
String refreshToken = (String) redisTemplate.opsForHash().get("userId-token:" + TokenKeyEnum.REFRESH_TOKEN_KEY.getValue(), user.getId().toString());
redisTemplate.delete(TokenKeyEnum.ACCESS_TOKEN_KEY.getValue() + token);
redisTemplate.delete(TokenKeyEnum.REFRESH_TOKEN_KEY.getValue() + refreshToken);
// 删除 userId-token 的hashmap
redisTemplate.opsForHash().delete("userId-token:" + TokenKeyEnum.ACCESS_TOKEN_KEY.getValue(), userId.toString());
redisTemplate.opsForHash().delete("userId-token:" + TokenKeyEnum.REFRESH_TOKEN_KEY.getValue(), userId.toString());
}
}
}

View File

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

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.security.manager;
package com.youlai.boot.core.security.token;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
@@ -38,7 +38,7 @@ import java.util.stream.Collectors;
* @author Ray.Hao
* @since 2024/11/15
*/
@ConditionalOnProperty(value = "security.auth.type", havingValue = "jwt")
@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
@Service
public class JwtTokenManager implements TokenManager {
@@ -49,7 +49,7 @@ public class JwtTokenManager implements TokenManager {
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
this.secretKey = securityProperties.getAuth().getJwtConfig().getKey().getBytes();
this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes();
}
/**
@@ -60,8 +60,8 @@ public class JwtTokenManager implements TokenManager {
*/
@Override
public AuthenticationToken generateToken(Authentication authentication) {
int accessTokenTimeToLive = securityProperties.getAuth().getAccessTokenTtl();
int refreshTokenTimeToLive = securityProperties.getAuth().getRefreshTokenTtl();
int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive();
String accessToken = generateToken(authentication, accessTokenTimeToLive);
String refreshToken = generateToken(authentication, refreshTokenTimeToLive);
@@ -175,7 +175,7 @@ public class JwtTokenManager implements TokenManager {
}
Authentication authentication = parseToken(refreshToken);
int accessTokenExpiration = securityProperties.getAuth().getRefreshTokenTtl();
int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive();
String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthenticationToken.builder()

View File

@@ -0,0 +1,229 @@
package com.youlai.boot.core.security.token;
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.common.result.ResultCode;
import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.core.security.model.OnlineUser;
import com.youlai.boot.core.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>
* 用于生成、解析、校验、刷新 JWT 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;
}
@Override
public AuthenticationToken generateToken(Authentication authentication) {
SysUserDetails user = (SysUserDetails) authentication.getPrincipal();
int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive();
// 生成随机令牌
String accessToken = IdUtil.fastSimpleUUID();
String refreshToken = IdUtil.fastSimpleUUID();
// 构建用户在线信息(不包含密码)
OnlineUser onlineUser = buildOnlineUser(user);
// 将访问令牌与刷新令牌与用户信息分别存入 Redis并设置过期时间
redisTemplate.opsForValue().set(
StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken),
onlineUser,
accessTtl,
TimeUnit.SECONDS
);
redisTemplate.opsForValue().set(
StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken),
onlineUser,
refreshTtl,
TimeUnit.SECONDS
);
// 单设备登录控制若不允许多设备登录则通过用户ID映射保存当前最新的访问令牌
Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin();
if (!allowMultiLogin) {
Long userId = user.getUserId();
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
// 获取当前用户已有的访问令牌
String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
if (oldAccessToken != null) {
// 删除旧的访问令牌对应的用户信息缓存
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken));
}
// 更新用户与访问令牌的映射
redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS);
}
// 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId());
redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS);
return AuthenticationToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(accessTtl)
.build();
}
/**
* 根据 token 解析用户信息
*
* @param token JWT Token
* @return
*/
@Override
public Authentication parseToken(String token) {
// 根据访问令牌从 Redis 中获取在线用户信息
String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey);
if (onlineUser == null) return null;
// 构建用户权限集合
Set<SimpleGrantedAuthority> authorities = onlineUser.getAuthorities().stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
// 构建用户详情对象
SysUserDetails userDetails = new SysUserDetails();
userDetails.setUserId(onlineUser.getUserId());
userDetails.setUsername(onlineUser.getUsername());
userDetails.setDeptId(onlineUser.getDeptId());
userDetails.setDataScope(onlineUser.getDataScope());
userDetails.setAuthorities(authorities);
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
}
/**
* 校验 Token 是否有效
*
* @param token 访问令牌
* @return
*/
@Override
public boolean validateToken(String token) {
String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
return redisTemplate.hasKey(tokenKey);
}
/**
* 刷新令牌
*
* @param refreshToken 刷新令牌
* @return
*/
@Override
public AuthenticationToken refreshToken(String refreshToken) {
// 根据刷新令牌获取在线用户信息
String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey);
if (onlineUser == null) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
}
// 获取当前用户的旧访问令牌(如果存在)
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId());
String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey);
if (oldAccessToken != null) {
// 删除旧的访问令牌记录
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken));
}
// 生成新访问令牌
String newAccessToken = IdUtil.fastSimpleUUID();
int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
redisTemplate.opsForValue().set(
StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken),
onlineUser,
accessTtl,
TimeUnit.SECONDS
);
// 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话)
redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS);
return AuthenticationToken.builder()
.accessToken(newAccessToken)
.refreshToken(refreshToken)
.expiresIn(accessTtl)
.build();
}
/**
* 将 Token 加入黑名单
*
* @param token 访问令牌
*/
@Override
public void blacklistToken(String token) {
// 删除访问令牌对应的在线用户信息缓存
String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey);
if (onlineUser != null) {
Long userId = onlineUser.getUserId();
// 删除访问令牌缓存和用户与访问令牌的映射
redisTemplate.delete(accessKey);
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId));
// 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存
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);
}
}
}
/**
* 构建 OnlineUser 对象
*/
private OnlineUser buildOnlineUser(SysUserDetails user) {
Long userId = user.getUserId();
Set<String> roles = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
return new OnlineUser(
userId,
user.getUsername(),
user.getDeptId(),
user.getDataScope(),
roles
);
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.security.manager;
package com.youlai.boot.core.security.token;
import com.youlai.boot.core.security.model.AuthenticationToken;
@@ -25,7 +25,7 @@ public interface TokenManager {
/**
* 解析 Token 获取认证信息
*
* @param token JWT Token
* @param token Token
* @return 用户认证信息
*/
Authentication parseToken(String token);

View File

@@ -17,7 +17,7 @@ import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.shared.auth.model.CaptchaInfo;
import com.youlai.boot.shared.auth.service.AuthService;
import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import com.youlai.boot.shared.sms.service.SmsService;
import lombok.RequiredArgsConstructor;

View File

@@ -12,7 +12,7 @@ import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.core.security.manager.TokenManager;
import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.core.security.service.PermissionService;
import com.youlai.boot.core.security.util.SecurityUtils;
import com.youlai.boot.shared.mail.service.MailService;