feat: 实现完整的 RBAC 权限管理系统与基础设施增强

在初始认证基础上,新增完整的 RBAC 权限模型(角色、权限、菜单三级管理),
  集成审计日志、接口限流、登录失败锁定、Refresh Token 机制、Redis 分布式缓存与锁、
  RocketMQ 消息队列,并引入 Flyway 数据库版本管理,同时补充项目文档与使用示例
This commit is contained in:
2026-04-10 10:58:22 +08:00
parent 3a9bf61839
commit 40c85c3c1f
97 changed files with 13434 additions and 351 deletions

View File

@@ -0,0 +1,363 @@
package com.aisi.template.service.impl;
import com.aisi.template.domain.entity.RefreshToken;
import com.aisi.template.domain.entity.User;
import com.aisi.template.domain.vo.LoginResponseVo;
import com.aisi.template.exception.BusinessException;
import com.aisi.template.repository.RefreshTokenRepository;
import com.aisi.template.repository.UserRepository;
import com.aisi.template.service.TokenService;
import com.aisi.template.utils.JwtUtil;
import com.aisi.template.utils.RedisUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.HexFormat;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Token 服务实现类
* 负责 Refresh Token 的生成、验证、刷新和黑名单管理
*
* 主要功能:
* 1. Refresh Token 管理:生成、存储、验证、撤销
* 2. Token 黑名单:将失效的 JWT 加入 Redis 黑名单
* 3. Token 刷新:使用 Refresh Token 获取新的 Access Token
* 4. 清理任务:清理过期的 Token 和黑名单记录
*
* @author Claude
* @since 2024-04-09
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TokenServiceImpl implements TokenService {
/**
* Refresh Token 数据访问接口
*/
private final RefreshTokenRepository refreshTokenRepository;
/**
* 用户数据访问接口
*/
private final UserRepository userRepository;
/**
* JWT 工具类
*/
private final JwtUtil jwtUtil;
/**
* Redis 工具类
*/
private final RedisUtils redisUtils;
/**
* Refresh Token 过期时间(秒),从配置读取,默认 7 天
*/
@Value("${jwt.refresh-token-expiration:604800}")
private long refreshTokenExpirationSeconds;
/**
* Access Token 过期时间(秒),从配置读取,默认 1 小时
*/
@Value("${jwt.access-token-expiration:3600}")
private long accessTokenExpirationSeconds;
/**
* Token 黑名单 Redis 键前缀
*/
private static final String BLACKLIST_KEY_PREFIX = "token:blacklist:";
/**
* Refresh Token Redis 键前缀
*/
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
/**
* 创建 Refresh Token
* 步骤:
* 1. 生成随机 UUID 作为 Token
* 2. 对 Token 进行 SHA-256 哈希(数据库只存储哈希值)
* 3. 计算 Token 过期时间
* 4. 保存到数据库
* 5. 同时在 Redis 中缓存一份(用于快速查找)
*
* @param userId 用户ID
* @param deviceInfo 设备信息iPhone 14 Pro
* @param ipAddress IP地址
* @return Refresh Token 原始值(未哈希)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String createRefreshToken(Long userId, String deviceInfo, String ipAddress) {
// 1. 生成随机的 Refresh Token
String refreshToken = UUID.randomUUID().toString().replace("-", "");
String tokenHash = hashToken(refreshToken);
// 2. 计算过期时间
LocalDateTime expiresAt = LocalDateTime.now().plusSeconds(refreshTokenExpirationSeconds);
// 3. 创建 Refresh Token 实体
RefreshToken tokenEntity = new RefreshToken();
tokenEntity.setUserId(userId);
tokenEntity.setTokenHash(tokenHash);
tokenEntity.setDeviceInfo(deviceInfo);
tokenEntity.setIpAddress(ipAddress);
tokenEntity.setExpiresAt(expiresAt);
tokenEntity.setRevoked(false);
// 4. 保存到数据库
refreshTokenRepository.save(tokenEntity);
// 5. 在 Redis 中缓存一份,过期时间与 Refresh Token 保持一致
// 作用:快速验证 Refresh Token减少数据库查询
redisUtils.set(REFRESH_TOKEN_PREFIX + tokenHash, String.valueOf(userId),
refreshTokenExpirationSeconds, TimeUnit.SECONDS);
log.info("Refresh Token 创建成功 - userId: {}, deviceInfo: {}", userId, deviceInfo);
return refreshToken;
}
/**
* 使用 Refresh Token 刷新 Access Token
* 步骤:
* 1. 对 Refresh Token 进行哈希
* 2. 先从 Redis 查找,找不到再查数据库
* 3. 验证 Token 是否有效(未过期、未撤销)
* 4. 获取用户信息并验证用户状态
* 5. 生成新的 Access Token
* 6. 生成新的 Refresh TokenToken 轮换机制)
* 7. 撤销旧的 Refresh Token
*
* @param refreshToken Refresh Token 原始值
* @return 登录响应(包含新的 Access Token 和 Refresh Token
* @throws BusinessException 当 Token 无效或用户状态异常时抛出异常
*/
@Override
@Transactional(rollbackFor = Exception.class)
public LoginResponseVo refreshToken(String refreshToken) {
// 1. 计算 Token 哈希值
String tokenHash = hashToken(refreshToken);
// 2. 先从 Redis 查找(快速路径)
String cachedUserId = redisUtils.get(REFRESH_TOKEN_PREFIX + tokenHash, String.class);
if (cachedUserId == null) {
// 2.1 Redis 没找到,查数据库(慢速路径)
RefreshToken tokenEntity = refreshTokenRepository.findByTokenHash(tokenHash)
.orElse(null);
if (tokenEntity == null) {
throw new BusinessException("Refresh Token 无效");
}
if (!tokenEntity.isValid()) {
throw new BusinessException("Refresh Token 已过期或已撤销");
}
}
// 3. 获取 Token 实体(从数据库)
RefreshToken tokenEntity = refreshTokenRepository.findByTokenHash(tokenHash)
.orElseThrow(() -> new BusinessException("Refresh Token 无效"));
// 4. 验证 Token 是否有效
if (!tokenEntity.isValid()) {
throw new BusinessException("Refresh Token 已过期或已撤销");
}
// 5. 获取用户信息
User user = userRepository.findById(tokenEntity.getUserId())
.orElseThrow(() -> new BusinessException("用户不存在"));
// 6. 检查用户状态
if (!user.isEnabled()) {
throw new BusinessException("用户账户已被禁用");
}
// 7. 生成新的 Access Token
String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername());
// 8. 生成新的 Refresh TokenToken 轮换)
// Token 轮换:每次刷新都生成新的 Refresh Token旧 Token 失效
// 好处:提高安全性,一旦 Token 泄露,使用后即失效
revokeRefreshToken(tokenHash);
String newRefreshToken = createRefreshToken(
user.getId(),
tokenEntity.getDeviceInfo(),
tokenEntity.getIpAddress()
);
// 9. 构建响应
LoginResponseVo response = LoginResponseVo.builder()
.accessToken(accessToken)
.refreshToken(newRefreshToken)
.expiresIn(accessTokenExpirationSeconds)
.userInfo(LoginResponseVo.UserInfoVo.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.build())
.build();
log.info("Token 刷新成功 - userId: {}", user.getId());
return response;
}
/**
* 撤销 Refresh Token
* 步骤:
* 1. 查找 Token 实体
* 2. 标记为已撤销
* 3. 记录撤销时间
* 4. 删除 Redis 缓存
*
* @param tokenHash Token 哈希值
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void revokeRefreshToken(String tokenHash) {
// 1. 查找 Token 实体
RefreshToken token = refreshTokenRepository.findByTokenHash(tokenHash)
.orElse(null);
if (token != null) {
// 2. 标记为已撤销
token.setRevoked(true);
token.setRevokedAt(LocalDateTime.now());
refreshTokenRepository.save(token);
// 3. 删除 Redis 缓存
redisUtils.delete(REFRESH_TOKEN_PREFIX + tokenHash);
log.info("Refresh Token 已撤销 - tokenHash: {}", tokenHash);
}
}
/**
* 撤销用户的所有 Refresh Token
* 步骤:
* 1. 将用户所有有效 Token 标记为已撤销
* 2. 删除对应的 Redis 缓存
*
* 使用场景:
* - 用户修改密码后,撤销所有 Token
* - 用户登出所有设备
* - 用户账户被禁用
*
* @param userId 用户ID
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void revokeAllUserTokens(Long userId) {
// 1. 查找用户所有有效 Token用于删除 Redis 缓存
var tokens = refreshTokenRepository.findValidTokensByUserId(userId, LocalDateTime.now());
// 2. 撤销数据库中所有 Token
refreshTokenRepository.revokeAllUserTokens(userId, LocalDateTime.now());
// 3. 删除 Redis 缓存
tokens.forEach(token -> redisUtils.delete(REFRESH_TOKEN_PREFIX + token.getTokenHash()));
log.info("用户所有 Refresh Token 已撤销 - userId: {}", userId);
}
/**
* 检查 Token 是否在黑名单中
* 步骤:
* 1. 构建 Redis 黑名单键
* 2. 检查键是否存在
*
* @param jti JWT IDJWT 唯一标识)
* @return 是否在黑名单中
*/
@Override
public boolean isTokenBlacklisted(String jti) {
String key = BLACKLIST_KEY_PREFIX + jti;
return Boolean.TRUE.equals(redisUtils.hasKey(key));
}
/**
* 将 Token 加入黑名单
* 步骤:
* 1. 构建 Redis 黑名单键
* 2. 设置黑名单记录,过期时间与 Token 剩余有效期一致
*
* 使用场景:
* - 用户登出时,将当前 Token 加入黑名单
* - Token 刷新时,将旧 Token 加入黑名单
*
* @param jti JWT ID
* @param expirationSeconds 过期时间(秒)
*/
@Override
public void addTokenToBlacklist(String jti, long expirationSeconds) {
String key = BLACKLIST_KEY_PREFIX + jti;
// 设置黑名单记录,过期时间与 Token 剩余有效期一致
// 作用:自动清理过期的黑名单记录,避免 Redis 内存溢出
redisUtils.set(key, "1", expirationSeconds, TimeUnit.SECONDS);
log.info("Token 已加入黑名单 - jti: {}, expire: {}s", jti, expirationSeconds);
}
/**
* 清理过期的 Refresh Token
* 步骤:
* 1. 查找所有过期的 Token
* 2. 批量删除
*
* 建议通过定时任务定期执行(如每天凌晨)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void cleanupExpiredTokens() {
LocalDateTime now = LocalDateTime.now();
refreshTokenRepository.deleteByExpiresAtBefore(now);
log.info("过期 Refresh Token 清理完成");
}
/**
* 清理过期的黑名单记录
* 说明:
* Redis 的黑名单记录设置了过期时间,会自动删除
* 此方法仅作为日志记录,实际无需执行
*/
@Override
public void cleanupExpiredBlacklistEntries() {
log.debug("黑名单记录由 Redis 自动过期清理");
}
/**
* 对 Token 进行 SHA-256 哈希
* 步骤:
* 1. 获取 SHA-256 算法实例
* 2. 对 Token 字节进行哈希
* 3. 转换为十六进制字符串
*
* 安全说明:
* - 数据库只存储哈希值,不存储原始 Token
* - 即使数据库泄露,攻击者也无法获得有效的 Refresh Token
*
* @param token 原始 Token
* @return 哈希后的 Token十六进制字符串
*/
private String hashToken(String token) {
try {
// 1. 获取 SHA-256 算法实例
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 2. 对 Token 进行哈希
byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
// 3. 转换为十六进制字符串
return HexFormat.of().formatHex(hash);
} catch (NoSuchAlgorithmException e) {
// 4. SHA-256 是 Java 标准库必须支持的算法,理论上不会抛出此异常
throw new RuntimeException("SHA-256 算法不可用", e);
}
}
}