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

@@ -4,84 +4,352 @@ import com.aisi.template.domain.RestBean;
import com.aisi.template.domain.dto.PageResult;
import com.aisi.template.domain.dto.PasswordResetConfirmDto;
import com.aisi.template.domain.dto.PasswordResetRequestDto;
import com.aisi.template.domain.dto.RefreshTokenDto;
import com.aisi.template.domain.dto.UserDto;
import com.aisi.template.domain.dto.UserQueryDto;
import com.aisi.template.domain.dto.UserRoleUpdateDto;
import com.aisi.template.domain.dto.UserStatusUpdateDto;
import com.aisi.template.domain.vo.LoginResponseVo;
import com.aisi.template.domain.vo.UserVo;
import com.aisi.template.service.PasswordResetService;
import com.aisi.template.service.TokenService;
import com.aisi.template.service.UserService;
import com.aisi.template.utils.JwtUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
/**
* 用户管理控制器
* 提供用户注册、登录、信息管理等相关接口
*
* 主要功能:
* 1. 用户认证注册、登录、登出、Token 刷新
* 2. 密码管理:密码重置(邮件验证码)
* 3. 用户信息查询:获取当前用户信息
* 4. 用户管理:用户列表查询(需要权限)、状态更新、角色分配
*
* 权限说明:
* - 用户注册、登录、密码重置:无需认证
* - 获取用户信息:需要登录
* - 用户管理接口:需要相应权限(如 user:list、user:update
*
* @author Claude
* @since 2024-04-09
*/
@RestController
@RequestMapping("/api/v1/user")
@RequiredArgsConstructor
@Tag(name = "用户接口")
@Tag(name = "用户接口", description = "用户注册、登录、信息管理等相关接口")
public class UserController {
/**
* 用户服务
* 处理用户相关的业务逻辑
*/
private final UserService userService;
/**
* 密码重置服务
* 处理密码重置的业务逻辑
*/
private final PasswordResetService passwordResetService;
/**
* Token 服务
* 处理 Refresh Token 和黑名单管理
*/
private final TokenService tokenService;
/**
* JWT 工具类
* 用于生成和解析 JWT Token
*/
private final JwtUtil jwtUtil;
/**
* 获取当前用户信息
* 步骤:
* 1. 从 SecurityContext 获取当前用户信息
* 2. 返回用户详细信息
*
* 权限需要登录Bearer Token
*
* @return 用户信息视图对象
*/
@GetMapping("info")
@Operation(summary = "用户信息")
@Operation(summary = "获取当前用户信息", description = "获取已登录用户的详细信息")
@SecurityRequirement(name = "Bearer Authentication")
public RestBean<UserVo> getUserInfo() {
return userService.getUserInfo();
}
/**
* 用户注册
* 步骤:
* 1. 校验用户名、邮箱是否已存在
* 2. 校验密码强度(必须包含大小写字母、数字、特殊字符)
* 3. 创建用户并分配默认角色ROLE_USER
* 4. 生成 Access Token 和 Refresh Token
*
* 注意:
* - 密码会使用 BCrypt 加密存储
* - 新用户默认分配 ROLE_USER 角色
*
* @param userDto 用户注册信息(用户名、密码、邮箱)
* @return 登录响应(包含 Token 和用户信息)
*/
@PostMapping("register")
@Operation(summary = "用户注册")
public RestBean<String> register(@Valid @RequestBody UserDto userDto) {
@Operation(summary = "用户注册", description = "创建新用户账号并返回登录凭证")
public RestBean<LoginResponseVo> register(@Valid @RequestBody UserDto userDto) {
return userService.register(userDto);
}
/**
* 用户登录
* 步骤:
* 1. 验证用户名和密码
* 2. 检查账户状态(是否被禁用、锁定)
* 3. 记录登录失败次数(超过阈值则锁定账户)
* 4. 生成 Access Token 和 Refresh Token
* 5. 发送登录消息到 MQ
*
* 注意:
* - 连续登录失败 5 次会锁定账户 30 分钟
* - 密码错误也会被记录
*
* @param userDto 登录信息(用户名、密码)
* @return 登录响应(包含 Token 和用户信息)
*/
@PostMapping("login")
@Operation(summary = "用户登录")
RestBean<String> login(@Valid @RequestBody UserDto userDto) {
@Operation(summary = "用户登录", description = "使用用户名密码登录,返回访问令牌")
RestBean<LoginResponseVo> login(@Valid @RequestBody UserDto userDto) {
return userService.login(userDto);
}
/**
* 用户登出
* 步骤:
* 1. 从请求头获取当前 Token
* 2. 提取 Token 的 JTIJWT ID
* 3. 将 Token 加入黑名单(剩余过期时间内有效)
* 4. 撤销用户所有 Refresh Token
*
* 权限需要登录Bearer Token
*
* @param request HTTP 请求对象(用于获取 Token
* @return 成功响应
*/
@PostMapping("logout")
@Operation(summary = "用户登出", description = "退出登录,使当前 Token 失效")
@SecurityRequirement(name = "Bearer Authentication")
public RestBean<Void> logout(HttpServletRequest request) {
// 1. 从请求头提取 Token
String token = extractToken(request);
if (token != null) {
// 2. 提取 Token 的 JTIJWT 唯一标识)
String jti = jwtUtil.extractJti(token);
if (jti != null) {
// 3. 计算剩余过期时间
long remainingSeconds = getRemainingExpiration(token);
// 4. 将 Token 加入黑名单(过期后自动删除)
tokenService.addTokenToBlacklist(jti, remainingSeconds);
}
// 5. 撤销当前用户的所有 Refresh Token
Long userId = jwtUtil.extractUserId(token);
if (userId != null) {
tokenService.revokeAllUserTokens(userId);
}
}
return RestBean.success();
}
/**
* 刷新访问令牌
* 步骤:
* 1. 验证 Refresh Token 是否有效
* 2. 生成新的 Access Token
* 3. 生成新的 Refresh TokenToken 轮换机制)
* 4. 撤销旧的 Refresh Token
*
* 注意:
* - Refresh Token 使用一次后即失效
* - 使用 Refresh Token 轮换提高安全性
*
* @param refreshTokenDto 刷新令牌请求
* @return 新的登录响应(包含新的 Token
*/
@PostMapping("refresh")
@Operation(summary = "刷新访问令牌", description = "使用 Refresh Token 获取新的 Access Token")
public RestBean<LoginResponseVo> refreshToken(@Valid @RequestBody RefreshTokenDto refreshTokenDto) {
LoginResponseVo response = tokenService.refreshToken(refreshTokenDto.getRefreshToken());
return RestBean.success(response);
}
/**
* 发送密码重置验证码
* 步骤:
* 1. 校验邮箱格式
* 2. 检查请求冷却时间(防止频繁请求)
* 3. 生成 6 位数字验证码
* 4. 发送验证码到用户邮箱
* 5. 验证码哈希后存储到 Redis10分钟有效
*
* 注意:
* - 同一邮箱 60 秒内只能请求一次
* - 验证码最多尝试 5 次
*
* @param requestDto 密码重置请求(邮箱)
* @return 成功响应
*/
@PostMapping("password-reset/request")
@Operation(summary = "发送找回密码验证码")
@Operation(summary = "发送密码重置验证码", description = "发送验证码到用户邮箱")
public RestBean<Void> sendPasswordResetCode(@Valid @RequestBody PasswordResetRequestDto requestDto) {
return passwordResetService.sendResetCode(requestDto);
}
/**
* 使用验证码重置密码
* 步骤:
* 1. 验证邮箱和验证码是否匹配
* 2. 验证验证码是否过期
* 3. 验证验证码尝试次数
* 4. 校验新密码强度
* 5. 更新用户密码
* 6. 删除已使用的验证码
*
* @param confirmDto 密码重置确认(邮箱、验证码、新密码)
* @return 成功响应
*/
@PostMapping("password-reset/confirm")
@Operation(summary = "验证码重置密码")
@Operation(summary = "验证码重置密码", description = "使用邮箱验证码重置用户密码")
public RestBean<Void> confirmPasswordReset(@Valid @RequestBody PasswordResetConfirmDto confirmDto) {
return passwordResetService.resetPassword(confirmDto);
}
/**
* 分页获取用户列表
* 步骤:
* 1. 根据查询条件构建动态查询
* 2. 支持按用户名/邮箱模糊搜索
* 3. 支持按状态筛选
* 4. 返回分页结果
*
* 权限:需要 user:list 权限
*
* @param queryDto 查询条件(关键词、状态等)
* @return 分页用户列表
*/
@GetMapping("list")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "分页获取用户列表")
@PreAuthorize("hasAuthority('user:list')")
@Operation(summary = "分页获取用户列表", description = "查询用户列表,支持分页和条件筛选")
@SecurityRequirement(name = "Bearer Authentication")
public RestBean<PageResult<UserVo>> getUserList(UserQueryDto queryDto) {
return userService.getUserList(queryDto);
}
/**
* 更新用户状态
* 步骤:
* 1. 检查用户是否存在
* 2. 禁止修改当前登录用户的状态
* 3. 更新用户状态(启用/禁用)
*
* 权限:需要 user:update 权限
*
* 注意:
* - 不能禁用当前登录的用户
* - 禁用后用户无法登录
*
* @param userId 用户ID
* @param updateDto 状态更新请求
* @return 更新后的用户信息
*/
@PutMapping("{userId}/status")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "更新用户状态")
@PreAuthorize("hasAuthority('user:update')")
@Operation(summary = "更新用户状态", description = "启用或禁用用户账号")
@SecurityRequirement(name = "Bearer Authentication")
public RestBean<UserVo> updateUserStatus(@PathVariable Long userId,
@Valid @RequestBody UserStatusUpdateDto updateDto) {
return userService.updateUserStatus(userId, updateDto);
}
/**
* 更新用户角色
* 步骤:
* 1. 检查用户是否存在
* 2. 禁止修改当前登录用户的角色
* 3. 验证角色ID是否存在
* 4. 更新用户角色关系
*
* 权限:需要 user:update 权限
*
* 注意:
* - 不能修改当前登录用户的角色
* - 角色变更后需要重新登录才能生效
*
* @param userId 用户ID
* @param updateDto 角色更新请求角色ID列表
* @return 更新后的用户信息
*/
@PutMapping("{userId}/role")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "更新用户角色")
@PreAuthorize("hasAuthority('user:update')")
@Operation(summary = "更新用户角色", description = "为用户分配或移除角色")
@SecurityRequirement(name = "Bearer Authentication")
public RestBean<UserVo> updateUserRole(@PathVariable Long userId,
@Valid @RequestBody UserRoleUpdateDto updateDto) {
return userService.updateUserRole(userId, updateDto);
}
/**
* 从 HTTP 请求中提取 JWT Token
* 步骤:
* 1. 从 Authorization 头获取值
* 2. 验证格式Bearer 前缀)
* 3. 提取 Token 部分(去掉 "Bearer " 前缀)
*
* @param request HTTP 请求对象
* @return JWT Token 字符串,无效返回 null
*/
private String extractToken(HttpServletRequest request) {
// 1. 获取 Authorization 头
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
return null;
}
// 2. 去掉 "Bearer " 前缀(注意空格)
return authorization.substring(7);
}
/**
* 计算 Token 剩余有效时间(秒)
* 步骤:
* 1. 提取 Token 过期时间
* 2. 计算与当前时间的差值
* 3. 转换为秒并确保不为负数
*
* @param token JWT Token 字符串
* @return 剩余秒数,已过期返回 0
*/
private long getRemainingExpiration(String token) {
try {
// 1. 获取过期时间
Date expiration = jwtUtil.extractExpiration(token);
// 2. 计算剩余毫秒数
long remainingMillis = expiration.getTime() - System.currentTimeMillis();
// 3. 转换为秒(至少为 0
return Math.max(0, remainingMillis / 1000);
} catch (Exception e) {
// Token 解析失败,返回 0
return 0;
}
}
}