Files
webgame-backend/src/main/java/com/webgame/webgamebackend/service/impl/RefreshTokenServiceImpl.java
2026-06-17 09:23:11 +08:00

165 lines
5.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.webgame.webgamebackend.service.impl;
import com.webgame.webgamebackend.common.exception.BusinessException;
import com.webgame.webgamebackend.common.exception.ErrorCode;
import com.webgame.webgamebackend.service.RefreshTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.concurrent.TimeUnit;
/**
* 刷新令牌服务实现,基于 Redis 存储
*
* <p>数据结构:</p>
* <ul>
* <li>xzg:refresh:{token} → userId正向映射用于校验</li>
* <li>xzg:refresh:user:{userId} → token反向映射用于登录时清除旧 token</li>
* </ul>
* <p>TTL: 30 天,由 Redis 自动过期</p>
* <p>一个用户同一时间只保留一个有效的 refresh token新登录会使旧 token 失效</p>
* <p>直接使用 StringRedisTemplate 存储纯字符串,避免 JSON 序列化导致的引号问题</p>
*/
@Slf4j
@Service
public class RefreshTokenServiceImpl implements RefreshTokenService {
/** Redis key 前缀 */
private static final String KEY_PREFIX = "xzg:refresh:";
/** 用户→token 反向映射 key 前缀 */
private static final String USER_KEY_PREFIX = "xzg:refresh:user:";
/** 刷新令牌有效期30 天 */
private static final long EXPIRE_DAYS = 30;
/** 安全随机数生成器 */
private static final SecureRandom RANDOM = new SecureRandom();
private final StringRedisTemplate stringRedisTemplate;
public RefreshTokenServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public String create(String userId) {
if (userId == null || userId.isBlank()) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "用户ID不能为空");
}
// 先清除该用户之前的 refresh token确保一个用户只有一个有效 token
String oldToken = get(USER_KEY_PREFIX + userId);
if (oldToken != null) {
delete(KEY_PREFIX + oldToken);
log.debug("已清除旧的刷新令牌: userId={}", userId);
}
// 创建新 token
String token = generateToken();
log.debug("刷新令牌已生成: token={}", token);
set(KEY_PREFIX + token, userId);
// 维护反向映射
set(USER_KEY_PREFIX + userId, token);
log.debug("刷新令牌已创建: userId={}", userId);
return token;
}
@Override
public String validate(String token) {
if (token == null || token.isBlank()) {
throw new BusinessException(ErrorCode.REFRESH_TOKEN_MISSING);
}
String userId = get(KEY_PREFIX + token);
if (userId == null) {
throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
}
return userId;
}
@Override
public String rotate(String oldToken) {
String userId = validate(oldToken);
// 先删反向映射,再删正向映射:
// 若反向映射删除失败,正向映射仍存在,调用方可重试;
// 若反向已删除而正向删除失败,正向映射仍存在,重试仍可通过。
delete(USER_KEY_PREFIX + userId);
delete(KEY_PREFIX + oldToken);
log.debug("旧刷新令牌已删除: userId={}", userId);
return create(userId);
}
@Override
public void revoke(String token) {
if (token == null || token.isBlank()) {
return;
}
try {
String userId = get(KEY_PREFIX + token);
if (userId != null) {
delete(USER_KEY_PREFIX + userId);
}
delete(KEY_PREFIX + token);
log.debug("刷新令牌已撤销");
} catch (BusinessException e) {
// 登出时 Redis 不可用,记录日志后放行,不影响用户登出
log.warn("撤销刷新令牌失败Redis 不可用),跳过清理: token={}", token, e);
}
}
/**
* 写入字符串到 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 读取字符串(读取时续期)
*
* @return 值key 不存在时返回 null
* @throws BusinessException 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);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "刷新令牌操作失败");
}
}
/**
* 从 Redis 删除 key
*
* @throws BusinessException Redis 操作失败时抛出
*/
private void delete(String key) {
try {
stringRedisTemplate.delete(key);
} catch (Exception e) {
log.error("删除缓存失败: key={}", key, e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "刷新令牌操作失败");
}
}
/**
* 生成 64 位十六进制随机字符串作为刷新令牌
*/
private String generateToken() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);
return HexFormat.of().formatHex(bytes);
}
}