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 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 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 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 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 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 = "发送密码重置验证码", description = "发送验证码到用户邮箱") public RestBean 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 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> 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 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 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; } } }