refactor: redis + token 会话重构,添加用户访问和刷新令牌映射,支持单设备登录控制和刷新令牌时让历史的访问令牌时效

This commit is contained in:
Ray.Hao
2025-03-11 18:36:39 +08:00
parent 27581fc060
commit b080d11853
7 changed files with 136 additions and 113 deletions

View File

@@ -1,4 +1,5 @@
package com.youlai.boot.config.property; package com.youlai.boot.config.property;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;

View File

@@ -41,5 +41,6 @@ public class OnlineUser {
/** /**
* 角色权限集合 * 角色权限集合
*/ */
private Set<String> authorities; private Set<String> roles;
} }

View File

@@ -131,7 +131,7 @@ public class JwtTokenManager implements TokenManager {
* @param token JWT Token * @param token JWT Token
*/ */
@Override @Override
public void blacklistToken(String token) { public void invalidateToken(String token) {
if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
} }

View File

@@ -1,5 +1,6 @@
package com.youlai.boot.core.security.token; package com.youlai.boot.core.security.token;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
@@ -33,12 +34,10 @@ import java.util.stream.Collectors;
@Service @Service
public class RedisTokenManager implements TokenManager { public class RedisTokenManager implements TokenManager {
// 安全配置属性
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate; private final RedisTemplate<String, Object> redisTemplate;
public RedisTokenManager(SecurityProperties securityProperties, public RedisTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) {
RedisTemplate<String, Object> redisTemplate) {
this.securityProperties = securityProperties; this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
} }
@@ -47,56 +46,35 @@ public class RedisTokenManager implements TokenManager {
* 生成 Token * 生成 Token
* *
* @param authentication 用户认证信息 * @param authentication 用户认证信息
* @return * @return 生成的 AuthenticationToken 对象
*/ */
@Override @Override
public AuthenticationToken generateToken(Authentication authentication) { public AuthenticationToken generateToken(Authentication authentication) {
SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); SysUserDetails user = (SysUserDetails) authentication.getPrincipal();
int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive();
// 生成随机令牌
String accessToken = IdUtil.fastSimpleUUID(); String accessToken = IdUtil.fastSimpleUUID();
String refreshToken = IdUtil.fastSimpleUUID(); String refreshToken = IdUtil.fastSimpleUUID();
// 构建用户在线信息(不包含密码) // 构建用户在线信息
OnlineUser onlineUser = buildOnlineUser(user); OnlineUser onlineUser = new OnlineUser(
user.getUserId(),
// 将访问令牌与刷新令牌与用户信息分别存入 Redis并设置过期时间 user.getUsername(),
setRedisValue( user.getDeptId(),
StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), user.getDataScope(),
onlineUser, user.getAuthorities().stream()
accessTtl .map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet())
); );
setRedisValue( // 存储访问令牌、刷新令牌和刷新令牌映射
StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), storeTokensInRedis(accessToken, refreshToken, onlineUser);
onlineUser,
refreshTtl
);
// 单设备登录控制若不允许多设备登录则通过用户ID映射保存当前最新的访问令牌 // 单设备登录控制
Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); handleSingleDeviceLogin(user.getUserId(), accessToken);
if (!allowMultiLogin) {
Long userId = user.getUserId();
String userAccessTokenKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
// 获取当前用户已有的访问令牌
String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessTokenKey);
if (oldAccessToken != null) {
// 删除旧的访问令牌对应的用户信息缓存
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken));
}
// 更新用户与访问令牌的映射
setRedisValue(userAccessTokenKey, accessToken, accessTtl);
}
// 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话
String userRefreshTokenKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId());
setRedisValue(userRefreshTokenKey, refreshToken, refreshTtl);
return AuthenticationToken.builder() return AuthenticationToken.builder()
.accessToken(accessToken) .accessToken(accessToken)
.refreshToken(refreshToken) .refreshToken(refreshToken)
.expiresIn(accessTtl) .expiresIn(securityProperties.getSession().getAccessTokenTimeToLive())
.build(); .build();
} }
@@ -104,29 +82,25 @@ public class RedisTokenManager implements TokenManager {
* 根据 token 解析用户信息 * 根据 token 解析用户信息
* *
* @param token JWT Token * @param token JWT Token
* @return * @return 构建的 Authentication 对象
*/ */
@Override @Override
public Authentication parseToken(String token) { public Authentication parseToken(String token) {
// 根据访问令牌从 Redis 中获取在线用户信息 OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey);
if (onlineUser == null) return null; if (onlineUser == null) return null;
// 构建用户权限集合 // 构建用户权限集合
Set<SimpleGrantedAuthority> authorities = onlineUser.getAuthorities().stream() Set<SimpleGrantedAuthority> authorities = null;
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet()); Set<String> roles = onlineUser.getRoles();
if (CollectionUtil.isNotEmpty(roles)) {
authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}
// 构建用户详情对象 // 构建用户详情对象
SysUserDetails userDetails = new SysUserDetails(); SysUserDetails userDetails = buildUserDetails(onlineUser, authorities);
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); return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
} }
@@ -134,51 +108,38 @@ public class RedisTokenManager implements TokenManager {
* 校验 Token 是否有效 * 校验 Token 是否有效
* *
* @param token 访问令牌 * @param token 访问令牌
* @return * @return 是否有效
*/ */
@Override @Override
public boolean validateToken(String token) { public boolean validateToken(String token) {
String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); return redisTemplate.hasKey(formatTokenKey(token));
return redisTemplate.hasKey(tokenKey);
} }
/** /**
* 刷新令牌 * 刷新令牌
* *
* @param refreshToken 刷新令牌 * @param refreshToken 刷新令牌
* @return * @return 新生成的 AuthenticationToken 对象
*/ */
@Override @Override
public AuthenticationToken refreshToken(String refreshToken) { public AuthenticationToken refreshToken(String refreshToken) {
// 根据刷新令牌获取在线用户信息 OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey);
if (onlineUser == null) { if (onlineUser == null) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
} }
// 获取当前用户的旧访问令牌(如果存在) String oldAccessToken = (String) redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()));
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId());
String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); // 删除旧的访问令牌记录
if (oldAccessToken != null) { if (oldAccessToken != null) {
// 删除旧的访问令牌记录 redisTemplate.delete(formatTokenKey(oldAccessToken));
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken));
} }
// 生成新访问令牌 // 生成新访问令牌并存储
String newAccessToken = IdUtil.fastSimpleUUID(); String newAccessToken = IdUtil.fastSimpleUUID();
storeAccessToken(newAccessToken, onlineUser);
int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); 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() return AuthenticationToken.builder()
.accessToken(newAccessToken) .accessToken(newAccessToken)
.refreshToken(refreshToken) .refreshToken(refreshToken)
@@ -187,24 +148,24 @@ public class RedisTokenManager implements TokenManager {
} }
/** /**
* 将 Token 加入黑名单 * 使访问令牌失效
* *
* @param token 访问令牌 * @param token 访问令牌
*/ */
@Override @Override
public void blacklistToken(String token) { public void invalidateToken(String token) {
// 删除访问令牌对应的在线用户信息缓存 OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey);
if (onlineUser != null) { if (onlineUser != null) {
Long userId = onlineUser.getUserId(); 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. 删除刷新令牌相关
redisTemplate.delete(accessKey);
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId));
// 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey);
if (refreshToken != null) { if (refreshToken != null) {
@@ -215,28 +176,91 @@ public class RedisTokenManager implements TokenManager {
} }
/** /**
* 构建 OnlineUser 对象 * 将访问令牌和刷新令牌存储至 Redis
*
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param onlineUser 在线用户信息
*/ */
private OnlineUser buildOnlineUser(SysUserDetails user) { private void storeTokensInRedis(String accessToken, String refreshToken, OnlineUser onlineUser) {
Long userId = user.getUserId(); // 访问令牌 -> 用户信息
Set<String> roles = user.getAuthorities().stream() setRedisValue(formatTokenKey(accessToken), onlineUser, securityProperties.getSession().getAccessTokenTimeToLive());
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet()); // 刷新令牌 -> 用户信息
return new OnlineUser( String refreshTokenKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
userId, setRedisValue(refreshTokenKey, onlineUser, securityProperties.getSession().getRefreshTokenTimeToLive());
user.getUsername(),
user.getDeptId(), // 用户ID -> 刷新令牌
user.getDataScope(), setRedisValue(StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, onlineUser.getUserId()),
roles refreshToken,
); securityProperties.getSession().getRefreshTokenTimeToLive());
} }
/** /**
* 将 Token 存储到 Redis * 处理单设备登录控制
*
* @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 key 键 * @param key 键
* @param value 值 * @param value 值
* @param ttl 过期时间(秒) * @param ttl 过期时间(秒)-1表示永不过期
*/ */
private void setRedisValue(String key, Object value, int ttl) { private void setRedisValue(String key, Object value, int ttl) {
if (ttl != -1) { if (ttl != -1) {
@@ -245,5 +269,4 @@ public class RedisTokenManager implements TokenManager {
redisTemplate.opsForValue().set(key, value); // ttl=-1时永不过期 redisTemplate.opsForValue().set(key, value); // ttl=-1时永不过期
} }
} }
} }

View File

@@ -30,7 +30,6 @@ public interface TokenManager {
*/ */
Authentication parseToken(String token); Authentication parseToken(String token);
/** /**
* 校验 Token 是否有效 * 校验 Token 是否有效
* *
@@ -39,7 +38,6 @@ public interface TokenManager {
*/ */
boolean validateToken(String token); boolean validateToken(String token);
/** /**
* 刷新 Token * 刷新 Token
* *
@@ -49,11 +47,11 @@ public interface TokenManager {
AuthenticationToken refreshToken(String token); AuthenticationToken refreshToken(String token);
/** /**
* Token 加入黑名单 * Token 失效
* *
* @param token JWT Token * @param token JWT Token
*/ */
default void blacklistToken(String token) { default void invalidateToken(String token) {
// 默认实现可以是空的,或者抛出不支持的操作异常 // 默认实现可以是空的,或者抛出不支持的操作异常
// throw new UnsupportedOperationException("Not implemented"); // throw new UnsupportedOperationException("Not implemented");
} }

View File

@@ -155,7 +155,7 @@ public class AuthServiceImpl implements AuthService {
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) { if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
// 将JWT令牌加入黑名单 // 将JWT令牌加入黑名单
tokenManager.blacklistToken(token); tokenManager.invalidateToken(token);
// 清除Security上下文 // 清除Security上下文
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
} }

View File

@@ -408,7 +408,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
if (result) { if (result) {
// 加入黑名单,重新登录 // 加入黑名单,重新登录
String accessToken = SecurityUtils.getTokenFromRequest(); String accessToken = SecurityUtils.getTokenFromRequest();
tokenManager.blacklistToken(accessToken); tokenManager.invalidateToken(accessToken);
} }
return result; return result;
} }