refactor(auth): 优化令牌管理和安全性验证逻辑
This commit is contained in:
@@ -152,10 +152,8 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void logout() {
|
public void logout() {
|
||||||
String token = SecurityUtils.getTokenFromRequest();
|
String token = SecurityUtils.getAccessToken();
|
||||||
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) {
|
if (StrUtil.isNotBlank(token)) {
|
||||||
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
|
|
||||||
// 将JWT令牌加入黑名单
|
|
||||||
tokenManager.invalidateToken(token);
|
tokenManager.invalidateToken(token);
|
||||||
// 清除Security上下文
|
// 清除Security上下文
|
||||||
SecurityContextHolder.clearContext();
|
SecurityContextHolder.clearContext();
|
||||||
|
|||||||
@@ -132,49 +132,46 @@ public class JwtTokenManager implements TokenManager {
|
|||||||
* @return 是否有效
|
* @return 是否有效
|
||||||
*/
|
*/
|
||||||
private boolean validateToken(String token, boolean validateRefreshToken) {
|
private boolean validateToken(String token, boolean validateRefreshToken) {
|
||||||
try {
|
JWT jwt = JWTUtil.parseToken(token);
|
||||||
JWT jwt = JWTUtil.parseToken(token);
|
// 检查 Token 是否有效(验签 + 是否过期)
|
||||||
// 检查 Token 是否有效(验签 + 是否过期)
|
boolean isValid = jwt.setKey(secretKey).validate(0);
|
||||||
boolean isValid = jwt.setKey(secretKey).validate(0);
|
|
||||||
|
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
JSONObject payloads = jwt.getPayloads();
|
JSONObject payloads = jwt.getPayloads();
|
||||||
// 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
|
// 1. 校验刷新令牌类型(仅在校验刷新令牌场景启用)
|
||||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||||
if (validateRefreshToken) {
|
if (validateRefreshToken) {
|
||||||
//刷新token需要校验token类别
|
//刷新token需要校验token类别
|
||||||
boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
|
boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE);
|
||||||
if (!isRefreshToken) {
|
if (!isRefreshToken) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 2. 校验安全版本号(用于按用户维度失效历史 Token)
|
|
||||||
Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
|
|
||||||
if (userId != null) {
|
|
||||||
// 老版本 Token 可能没有 SECURITY_VERSION 声明,视为 0 版本
|
|
||||||
Integer tokenVersionRaw = payloads.getInt(JwtClaimConstants.SECURITY_VERSION);
|
|
||||||
int tokenVersion = tokenVersionRaw != null ? tokenVersionRaw : 0;
|
|
||||||
|
|
||||||
String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId);
|
|
||||||
Integer currentVersionRaw = (Integer) redisTemplate.opsForValue().get(versionKey);
|
|
||||||
int currentVersion = currentVersionRaw != null ? currentVersionRaw : 0;
|
|
||||||
|
|
||||||
// 如果当前版本号比 Token 携带的版本号新,则认为该 Token 已失效
|
|
||||||
if (tokenVersion < currentVersion) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 判断是否在黑名单中,如果在,则返回 false 标识Token无效
|
|
||||||
if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isValid;
|
// 2. 校验安全版本号(用于按用户维度失效历史 Token)
|
||||||
} catch (Exception gitignore) {
|
// 场景示例:用户修改密码、被管理员强制下线、手动“踢所有端”后,将用户安全版本号 +1,旧版本 Token 全部失效
|
||||||
// token 验证
|
Long userId = payloads.getLong(JwtClaimConstants.USER_ID);
|
||||||
|
if (userId != null) {
|
||||||
|
// 老版本 Token 可能没有 SECURITY_VERSION 声明,视为 0 版本
|
||||||
|
Integer tokenVersionRaw = payloads.getInt(JwtClaimConstants.SECURITY_VERSION);
|
||||||
|
int tokenVersion = tokenVersionRaw != null ? tokenVersionRaw : 0;
|
||||||
|
|
||||||
|
String versionKey = StrUtil.format(RedisConstants.Auth.USER_SECURITY_VERSION, userId);
|
||||||
|
Integer currentVersionRaw = (Integer) redisTemplate.opsForValue().get(versionKey);
|
||||||
|
int currentVersion = currentVersionRaw != null ? currentVersionRaw : 0;
|
||||||
|
|
||||||
|
// 如果当前版本号比 Token 携带的版本号新,则认为该 Token 已失效
|
||||||
|
if (tokenVersion < currentVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 判断是否在黑名单中,如果在,则返回 false 标识Token无效
|
||||||
|
// 场景示例:单点退出登录、后台手动注销某个会话、封禁账号后立即阻断当前 Token 等
|
||||||
|
if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,7 +207,6 @@ public class JwtTokenManager implements TokenManager {
|
|||||||
// 永不过期的Token永久加入黑名单
|
// 永不过期的Token永久加入黑名单
|
||||||
redisTemplate.opsForValue().set(blacklistTokenKey, Boolean.TRUE);
|
redisTemplate.opsForValue().set(blacklistTokenKey, Boolean.TRUE);
|
||||||
}
|
}
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -165,8 +165,8 @@ public class RedisTokenManager implements TokenManager {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void invalidateToken(String token) {
|
public void invalidateToken(String token) {
|
||||||
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(formatTokenKey(token));
|
Object value = redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||||
if (onlineUser != null) {
|
if (value instanceof OnlineUser onlineUser) {
|
||||||
Long userId = onlineUser.getUserId();
|
Long userId = onlineUser.getUserId();
|
||||||
invalidateUserSessions(userId);
|
invalidateUserSessions(userId);
|
||||||
}
|
}
|
||||||
@@ -186,20 +186,18 @@ public class RedisTokenManager implements TokenManager {
|
|||||||
// 1. 删除访问令牌相关
|
// 1. 删除访问令牌相关
|
||||||
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
|
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
|
||||||
Object accessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
Object accessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
||||||
Optional.of(accessTokenValue)
|
if (accessTokenValue instanceof String accessToken) {
|
||||||
.map(String.class::cast)
|
redisTemplate.delete(formatTokenKey(accessToken));
|
||||||
.ifPresent(accessToken -> redisTemplate.delete(formatTokenKey(accessToken)));
|
}
|
||||||
// 无论是否存在访问令牌映射,都尝试删除 userAccessKey
|
// 无论是否存在访问令牌映射,都尝试删除 userAccessKey
|
||||||
redisTemplate.delete(userAccessKey);
|
redisTemplate.delete(userAccessKey);
|
||||||
|
|
||||||
// 2. 删除刷新令牌相关
|
// 2. 删除刷新令牌相关
|
||||||
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
|
String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId);
|
||||||
Object refreshTokenValue = redisTemplate.opsForValue().get(userRefreshKey);
|
Object refreshTokenValue = redisTemplate.opsForValue().get(userRefreshKey);
|
||||||
Optional.of(refreshTokenValue)
|
if (refreshTokenValue instanceof String refreshToken) {
|
||||||
.map(String.class::cast)
|
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
|
||||||
.ifPresent(refreshToken ->
|
}
|
||||||
redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken))
|
|
||||||
);
|
|
||||||
// 同样清理 userRefreshKey 本身
|
// 同样清理 userRefreshKey 本身
|
||||||
redisTemplate.delete(userRefreshKey);
|
redisTemplate.delete(userRefreshKey);
|
||||||
}
|
}
|
||||||
@@ -237,9 +235,9 @@ public class RedisTokenManager implements TokenManager {
|
|||||||
// 单设备登录控制,删除旧的访问令牌
|
// 单设备登录控制,删除旧的访问令牌
|
||||||
if (!allowMultiLogin) {
|
if (!allowMultiLogin) {
|
||||||
Object oldAccessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
Object oldAccessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
|
||||||
Optional.of(oldAccessTokenValue)
|
if (oldAccessTokenValue instanceof String oldAccessToken) {
|
||||||
.map(String.class::cast)
|
redisTemplate.delete(formatTokenKey(oldAccessToken));
|
||||||
.ifPresent(oldAccessToken -> redisTemplate.delete(formatTokenKey(oldAccessToken)));
|
}
|
||||||
}
|
}
|
||||||
// 存储访问令牌映射(用户ID -> 访问令牌),用于单设备登录控制删除旧的访问令牌和刷新令牌时删除旧令牌
|
// 存储访问令牌映射(用户ID -> 访问令牌),用于单设备登录控制删除旧的访问令牌和刷新令牌时删除旧令牌
|
||||||
setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
|
setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ public class SecurityUtils {
|
|||||||
*
|
*
|
||||||
* @return Token 字符串
|
* @return Token 字符串
|
||||||
*/
|
*/
|
||||||
public static String getTokenFromRequest() {
|
public static String getAccessToken() {
|
||||||
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
|
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
|
||||||
if(Objects.isNull(servletRequestAttributes)) {
|
if(Objects.isNull(servletRequestAttributes)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user