Files
springboot-template/src/main/java/com/aisi/template/controller/UserController.java
shenjianZ 40c85c3c1f feat: 实现完整的 RBAC 权限管理系统与基础设施增强
在初始认证基础上,新增完整的 RBAC 权限模型(角色、权限、菜单三级管理),
  集成审计日志、接口限流、登录失败锁定、Refresh Token 机制、Redis 分布式缓存与锁、
  RocketMQ 消息队列,并引入 Flyway 数据库版本管理,同时补充项目文档与使用示例
2026-04-10 10:58:22 +08:00

356 lines
12 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.aisi.template.controller;
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 = "用户接口", 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 = "获取当前用户信息", 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 = "用户注册", 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 = "用户登录", 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 = "发送密码重置验证码", 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 = "验证码重置密码", 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("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("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("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;
}
}
}