feat: 实现完整的 RBAC 权限管理系统与基础设施增强
在初始认证基础上,新增完整的 RBAC 权限模型(角色、权限、菜单三级管理), 集成审计日志、接口限流、登录失败锁定、Refresh Token 机制、Redis 分布式缓存与锁、 RocketMQ 消息队列,并引入 Flyway 数据库版本管理,同时补充项目文档与使用示例
This commit is contained in:
@@ -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 的 JTI(JWT 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 的 JTI(JWT 唯一标识)
|
||||
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 Token(Token 轮换机制)
|
||||
* 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. 验证码哈希后存储到 Redis(10分钟有效)
|
||||
*
|
||||
* 注意:
|
||||
* - 同一邮箱 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user