feat: token

This commit is contained in:
2026-06-16 18:20:11 +08:00
parent 5026c9b39c
commit b9ed1cd326
2 changed files with 63 additions and 22 deletions

View File

@@ -2,14 +2,14 @@ package com.webgame.webgamebackend.service.impl;
import com.webgame.webgamebackend.common.exception.BusinessException; import com.webgame.webgamebackend.common.exception.BusinessException;
import com.webgame.webgamebackend.common.exception.ErrorCode; import com.webgame.webgamebackend.common.exception.ErrorCode;
import com.webgame.webgamebackend.common.utils.RedisCacheUtil;
import com.webgame.webgamebackend.service.RefreshTokenService; import com.webgame.webgamebackend.service.RefreshTokenService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.concurrent.TimeUnit;
/** /**
* 刷新令牌服务实现,基于 Redis 存储 * 刷新令牌服务实现,基于 Redis 存储
@@ -21,43 +21,44 @@ import java.util.HexFormat;
* </ul> * </ul>
* <p>TTL: 30 天,由 Redis 自动过期</p> * <p>TTL: 30 天,由 Redis 自动过期</p>
* <p>一个用户同一时间只保留一个有效的 refresh token新登录会使旧 token 失效</p> * <p>一个用户同一时间只保留一个有效的 refresh token新登录会使旧 token 失效</p>
* <p>直接使用 StringRedisTemplate 存储纯字符串,避免 JSON 序列化导致的引号问题</p>
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
public class RefreshTokenServiceImpl implements RefreshTokenService { public class RefreshTokenServiceImpl implements RefreshTokenService {
/** Redis key 前缀 */ /** Redis key 前缀 */
private static final String PREFIX = "refresh"; private static final String KEY_PREFIX = "xzg:refresh:";
/** 用户→token 反向映射 key 前缀 */ /** 用户→token 反向映射 key 前缀 */
private static final String USER_PREFIX = "refresh:user"; private static final String USER_KEY_PREFIX = "xzg:refresh:user:";
/** 刷新令牌有效期30 天(毫秒) */ /** 刷新令牌有效期30 天 */
private static final long EXPIRE_MS = 2_592_000_000L; private static final long EXPIRE_DAYS = 30;
/** 安全随机数生成器 */ /** 安全随机数生成器 */
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();
private final RedisCacheUtil redisCacheUtil; private final StringRedisTemplate stringRedisTemplate;
public RefreshTokenServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override @Override
public String create(String userId) { public String create(String userId) {
// 先清除该用户之前的 refresh token确保一个用户只有一个有效 token // 先清除该用户之前的 refresh token确保一个用户只有一个有效 token
String oldToken = redisCacheUtil.getCache(USER_PREFIX, userId, String.class, EXPIRE_MS); String oldToken = get(USER_KEY_PREFIX + userId);
if (oldToken != null) { if (oldToken != null) {
redisCacheUtil.removeCache(PREFIX, oldToken); delete(KEY_PREFIX + oldToken);
log.debug("已清除旧的刷新令牌: userId={}", userId); log.debug("已清除旧的刷新令牌: userId={}", userId);
} }
// 创建新 token // 创建新 token
String token = generateToken(); String token = generateToken();
boolean ok = redisCacheUtil.cacheValue(PREFIX, token, userId, EXPIRE_MS); log.debug("刷新令牌已生成: token={}", token);
if (!ok) { set(KEY_PREFIX + token, userId);
log.error("刷新令牌写入 Redis 失败: userId={}", userId);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "刷新令牌生成失败");
}
// 维护反向映射 // 维护反向映射
redisCacheUtil.cacheValue(USER_PREFIX, userId, token, EXPIRE_MS); set(USER_KEY_PREFIX + userId, token);
log.debug("刷新令牌已创建: userId={}", userId); log.debug("刷新令牌已创建: userId={}", userId);
return token; return token;
} }
@@ -67,7 +68,7 @@ public class RefreshTokenServiceImpl implements RefreshTokenService {
if (token == null || token.isBlank()) { if (token == null || token.isBlank()) {
throw new BusinessException(ErrorCode.REFRESH_TOKEN_MISSING); throw new BusinessException(ErrorCode.REFRESH_TOKEN_MISSING);
} }
String userId = redisCacheUtil.getCache(PREFIX, token, String.class, EXPIRE_MS); String userId = get(KEY_PREFIX + token);
if (userId == null) { if (userId == null) {
throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID); throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
} }
@@ -77,8 +78,8 @@ public class RefreshTokenServiceImpl implements RefreshTokenService {
@Override @Override
public String rotate(String oldToken) { public String rotate(String oldToken) {
String userId = validate(oldToken); String userId = validate(oldToken);
redisCacheUtil.removeCache(PREFIX, oldToken); delete(KEY_PREFIX + oldToken);
redisCacheUtil.removeCache(USER_PREFIX, userId); delete(USER_KEY_PREFIX + userId);
log.debug("旧刷新令牌已删除: userId={}", userId); log.debug("旧刷新令牌已删除: userId={}", userId);
return create(userId); return create(userId);
} }
@@ -86,15 +87,54 @@ public class RefreshTokenServiceImpl implements RefreshTokenService {
@Override @Override
public void revoke(String token) { public void revoke(String token) {
if (token != null && !token.isBlank()) { if (token != null && !token.isBlank()) {
String userId = redisCacheUtil.getCache(PREFIX, token, String.class, EXPIRE_MS); String userId = get(KEY_PREFIX + token);
if (userId != null) { if (userId != null) {
redisCacheUtil.removeCache(USER_PREFIX, userId); delete(USER_KEY_PREFIX + userId);
} }
redisCacheUtil.removeCache(PREFIX, token); delete(KEY_PREFIX + token);
log.debug("刷新令牌已撤销"); log.debug("刷新令牌已撤销");
} }
} }
/**
* 写入字符串到 Redis带过期时间
*/
private void set(String key, String value) {
try {
stringRedisTemplate.opsForValue().set(key, value, EXPIRE_DAYS, TimeUnit.DAYS);
} catch (Exception e) {
log.error("刷新令牌写入 Redis 失败: key={}", key, e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "刷新令牌操作失败");
}
}
/**
* 从 Redis 读取字符串(读取时续期)
*/
private String get(String key) {
try {
String value = stringRedisTemplate.opsForValue().get(key);
if (value != null) {
stringRedisTemplate.expire(key, EXPIRE_DAYS, TimeUnit.DAYS);
}
return value;
} catch (Exception e) {
log.error("查询缓存失败: key={}", key, e);
return null;
}
}
/**
* 从 Redis 删除 key
*/
private void delete(String key) {
try {
stringRedisTemplate.delete(key);
} catch (Exception e) {
log.error("删除缓存失败: key={}", key, e);
}
}
/** /**
* 生成 64 位十六进制随机字符串作为刷新令牌 * 生成 64 位十六进制随机字符串作为刷新令牌
*/ */

View File

@@ -24,6 +24,7 @@ public class UserServiceImpl implements UserService {
@Override @Override
public UserInfoResponse getCurrentUserInfo() { public UserInfoResponse getCurrentUserInfo() {
String accountId = StpUtil.getLoginIdAsString(); String accountId = StpUtil.getLoginIdAsString();
log.info("[用户服务] 获取当前登录用户信息, accountId: {}", accountId);
UserEntity user = userRepository.findByAccountId(accountId) UserEntity user = userRepository.findByAccountId(accountId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
return UserInfoResponse.from(user); return UserInfoResponse.from(user);