
登录功能架构文档
本文档详细讲解 CLX 项目登录功能涉及的所有后端类及其职责。
一、整体架构
┌─────────────────────────────────────────────────────────────────────┐
│ 前端请求 │
└────────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AuthController (controller/AuthController.java:38) │
│ - 接收 HTTP 请求 │
│ - 参数校验 (@Valid) │
│ - 调用 AuthService │
└────────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AuthServiceImpl (service/impl/AuthServiceImpl.java:37) │
│ - 核心业务逻辑 │
│ - 验证码校验 → CaptchaService │
│ - 用户查询 → UserMapper │
│ - 密码校验 → BCryptPasswordEncoder │
│ - 登录状态 → sa-Token (StpUtil) │
│ - 失败计数 → Redis │
└────────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 底层依赖 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ MySQL │ │ Redis │ │ sa-Token │ │
│ │ (用户数据) │ │ (Token存储) │ │ (JWT生成) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
二、涉及类清单
| 层级 | 类名 | 路径 | 职责 |
|---|---|---|---|
| Controller | AuthController |
clx-auth/.../controller/AuthController.java |
HTTP 接口入口 |
| DTO | LoginRequest |
clx-auth/.../dto/LoginRequest.java |
登录请求参数 |
| VO | LoginVO |
clx-auth/.../vo/LoginVO.java |
登录返回结果 |
| VO | UserInfoVO |
clx-auth/.../vo/UserInfoVO.java |
用户信息返回 |
| Service | AuthService |
clx-auth/.../service/AuthService.java |
认证接口定义 |
| Service | AuthServiceImpl |
clx-auth/.../service/impl/AuthServiceImpl.java |
认证核心实现 |
| Service | CaptchaService |
clx-auth/.../service/CaptchaService.java |
图形验证码服务 |
| Entity | User |
clx-auth/.../entity/User.java |
用户实体 |
| Mapper | UserMapper |
clx-auth/.../mapper/UserMapper.java |
数据访问接口 |
| XML | UserMapper.xml |
clx-auth/.../resources/mapper/UserMapper.xml |
SQL 映射 |
| Common | R |
clx-common-core/.../domain/R.java |
统一响应封装 |
| Common | AuthException |
clx-common-core/.../exception/AuthException.java |
认证异常 |
| Common | TokenConstants |
clx-common-core/.../constant/TokenConstants.java |
Token 常量 |
| Common | SecurityConstants |
clx-common-core/.../constant/SecurityConstants.java |
安全常量 |
| Security | SaTokenConfig |
clx-common-security/.../config/SaTokenConfig.java |
BCrypt、CORS 配置 |
| Security | SaTokenJwtConfig |
clx-common-security/.../config/SaTokenJwtConfig.java |
JWT 模式配置 |
| Security | StpInterfaceImpl |
clx-common-security/.../config/StpInterfaceImpl.java |
权限接口(暂空) |
| Security | SaTokenExceptionHandler |
clx-common-security/.../exception/SaTokenExceptionHandler.java |
异常处理 |
三、类详细讲解
3.1 Controller 层
AuthController
路径 : clx-auth/src/main/java/com/clx/auth/controller/AuthController.java
职责: HTTP 接口入口,接收请求、参数校验、调用 Service
java
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
private final CaptchaService captchaService;
private final VerificationCodeService verificationCodeService;
private final EmailService emailService;
登录接口 (第45-61行):
java
@PostMapping("/login")
public R<LoginVO> login(@Valid @RequestBody LoginRequest request, HttpServletRequest servletRequest) {
// ① 用户名标准化(去空格)
String username = request.username() == null ? "" : request.username().trim();
// ② 处理 rememberMe(可能为 null)
boolean rememberMe = Boolean.TRUE.equals(request.rememberMe());
// ③ 解析客户端 IP(支持代理场景)
String clientIp = resolveClientIp(servletRequest);
// ④ 调用 Service 并返回统一响应
return R.ok(authService.login(username, request.password(),
request.captchaId(), request.captchaCode(), rememberMe, clientIp));
}
IP 解析方法 (第167-179行):
java
private String resolveClientIp(HttpServletRequest request) {
// 优先读取代理传递的 IP
String forwardedFor = request.getHeader("X-Forwarded-For");
if (forwardedFor != null && !forwardedFor.isBlank()) {
return forwardedFor.split(",")[0].trim(); // 多层代理取第一个
}
String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) {
return realIp.trim();
}
// 最后取直接连接的 IP
return request.getRemoteAddr();
}
3.2 DTO 层
LoginRequest
路径 : clx-auth/src/main/java/com/clx/auth/dto/LoginRequest.java
职责: 定义登录请求参数,使用 Java 17 record 类型
java
public record LoginRequest(
@NotBlank(message = "用户名不能为空")
@Size(max = 50, message = "用户名长度不能超过50个字符")
String username,
@NotBlank(message = "密码不能为空")
@Size(max = 128, message = "密码长度不能超过128个字符")
String password,
@NotBlank(message = "图形验证码ID不能为空")
String captchaId,
@NotBlank(message = "图形验证码不能为空")
@Size(min = 4, max = 4, message = "图形验证码必须是4位")
String captchaCode,
Boolean rememberMe // 可为 null
) {}
注解说明:
@NotBlank: 不能为空字符串@Size: 长度限制@Valid: Controller 使用此注解触发校验
3.3 VO 层
LoginVO
路径 : clx-auth/src/main/java/com/clx/auth/vo/LoginVO.java
职责: 登录成功后返回给前端的数据结构
java
public record LoginVO(
String token, // JWT Token(三段式 base64 字符串)
String tokenName, // Token 名称,固定为 "Authorization"
long tokenTimeout, // Token 绝对有效期(秒)
long activeTimeout, // Token 活跃有效期(无操作后过期秒数)
boolean rememberMe // 是否开启了"记住我"
) {}
字段详解:
token: JWT 格式,如eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.xxx.xxxtokenTimeout: Token 的最大存活时间,普通登录 4 小时,记住我 30 天activeTimeout: 无操作后自动过期时间,普通登录 2 小时,记住我 7 天
UserInfoVO
路径 : clx-auth/src/main/java/com/clx/auth/vo/UserInfoVO.java
职责 : 获取当前用户信息接口 (GET /auth/me) 的返回结构
java
public record UserInfoVO(
Long userId, // 用户 ID
String username, // 用户名
Object tokenInfo // sa-Token 的 Token 详细信息(包含过期时间等)
) {}
3.4 Service 层
AuthService (接口)
路径 : clx-auth/src/main/java/com/clx/auth/service/AuthService.java
职责: 定义认证服务的接口契约
java
public interface AuthService {
LoginVO login(String username, String password, String captchaId,
String captchaCode, boolean rememberMe, String clientIp);
RegisterVO register(...);
void logout();
UserInfoVO getCurrentUser();
LoginVO refreshToken();
boolean existsByEmail(String email);
void resetPassword(String email, String newPassword);
}
AuthServiceImpl (核心实现)
路径 : clx-auth/src/main/java/com/clx/auth/service/impl/AuthServiceImpl.java
职责: 登录核心业务逻辑实现
依赖注入 (第42-48行):
java
private final UserMapper userMapper; // 用户数据访问
private final BCryptPasswordEncoder passwordEncoder; // 密码加密器
private final StringRedisTemplate redisTemplate; // Redis 操作
private final SaTokenConfig saTokenConfig; // sa-Token 配置
private final RememberMeProperties rememberMeProperties; // 记住我配置
private final CaptchaService captchaService; // 验证码服务
private final VerificationCodeService verificationCodeService; // 验证码服务
login 方法核心逻辑 (第50-102行):
java
public LoginVO login(...) {
// ① 用户名标准化:转小写、去空格
String normalizedUsername = normalizeUsername(username);
String attemptKey = getAttemptKey(normalizedUsername);
// ② 检查登录锁定(防止暴力破解)
checkLoginLock(attemptKey);
// ③ 验证图形验证码
if (!captchaService.verifyCaptchaCode(captchaId, captchaCode)) {
throw AuthException.captchaError();
}
// ④ 查询用户
User user = userMapper.selectByUsername(normalizedUsername);
// ⑤ 密码校验(关键:防时序攻击)
if (!isPasswordMatched(user, password)) {
recordFailure(attemptKey); // 记录失败
throw AuthException.loginFailed();
}
// ⑥ 账户状态检查
if (user.isDeleted()) { recordFailure(attemptKey); throw AuthException.loginFailed(); }
if (user.isDisabled()) { throw AuthException.accountDisabled(); }
if (user.isLocked()) { throw AuthException.accountLocked(); }
// ⑦ 计算有效期(rememberMe 影响超时时间)
long loginTimeout = resolveLoginTimeout(rememberMe);
long activeTimeout = resolveActiveTimeout(rememberMe, loginTimeout);
// ⑧ sa-Token 登录(核心)
StpUtil.login(user.getUserId(), SaLoginModel.create()
.setTimeout(loginTimeout)
.setActiveTimeout(activeTimeout)
.setIsLastingCookie(rememberMe));
// ⑨ 存储会话信息到 Session
StpUtil.getSession().set("username", user.getUsername());
StpUtil.getSession().set("nickname", user.getNickname());
StpUtil.getSession().set("rememberMe", rememberMe);
// ⑩ 清除失败计数
clearFailures(attemptKey);
// ⑪ 更新登录信息(IP、时间、次数)
userMapper.updateLoginSuccess(user.getUserId(), clientIp);
// ⑫ 返回结果
return new LoginVO(StpUtil.getTokenValue(), SecurityConstants.TOKEN_HEADER, ...);
}
防时序攻击的密码校验 (第228-231行):
java
private static final String DUMMY_PASSWORD_HASH =
"$2a$10$cX1Bgw3VdxwApyokYRF3B.iYYKD5IOu/8siinuC.M6NkQSIW7A4we";
private boolean isPasswordMatched(User user, String rawPassword) {
// 关键:用户不存在时也执行密码比对,防止通过响应时间判断用户是否存在
String encodedPassword = user == null ? DUMMY_PASSWORD_HASH : user.getPassword();
return passwordEncoder.matches(rawPassword, encodedPassword) && user != null;
}
登录失败锁定逻辑 (第233-272行):
java
private void checkLoginLock(String attemptKey) {
Long count = readFailureCount(attemptKey);
if (count >= TokenConstants.MAX_LOGIN_ATTEMPT) { // 5 次
throw AuthException.tooManyAttempts();
}
}
private void recordFailure(String attemptKey) {
Long count = redisTemplate.opsForValue().increment(attemptKey);
if (count != null) {
redisTemplate.expire(attemptKey, TokenConstants.LOGIN_LOCK_TIME, TimeUnit.SECONDS); // 30分钟
}
}
private String getAttemptKey(String normalizedUsername) {
return TokenConstants.LOGIN_ATTEMPT_KEY + normalizedUsername; // "clx:auth:attempt:admin"
}
rememberMe 超时计算 (第278-290行):
java
private long resolveLoginTimeout(boolean rememberMe) {
return rememberMe
? rememberMeProperties.getTimeout() // 记住我:30天
: saTokenConfig.getTimeout(); // 普通:4小时
}
private long resolveActiveTimeout(boolean rememberMe, long loginTimeout) {
long activeTimeout = rememberMe
? rememberMeProperties.getActiveTimeout() // 记住我:7天
: saTokenConfig.getActiveTimeout(); // 普通:2小时
// 活跃超时不能超过绝对超时
if (loginTimeout > 0 && activeTimeout > loginTimeout) {
return loginTimeout;
}
return activeTimeout;
}
3.5 Entity 层
User
路径 : clx-auth/src/main/java/com/clx/auth/entity/User.java
职责 : 用户实体,对应数据库表 sys_user
java
public class User {
private Long userId; // 用户ID,主键自增
private String username; // 用户名,唯一索引
private String password; // 密码,BCrypt加密(60字符)
private String nickname; // 昵称
private String email; // 邮箱
private String phone; // 手机号
private String status; // 状态:0正常,1禁用,2锁定
private Integer isDeleted; // 删除标记:0未删除,1已删除
// 状态判断方法
public boolean isDisabled() { return StatusConstants.DISABLED.equals(status); }
public boolean isLocked() { return StatusConstants.LOCKED.equals(status); }
public boolean isDeleted() { return Integer.valueOf(StatusConstants.DELETED).equals(isDeleted); }
}
3.6 Mapper 层
UserMapper
路径 : clx-auth/src/main/java/com/clx/auth/mapper/UserMapper.java
职责: MyBatis Mapper 接口,定义用户数据访问方法
java
@Mapper
public interface UserMapper {
// 登录时使用:根据用户名查询用户
User selectByUsername(@Param("username") String username);
// 权限相关(暂未使用)
List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
List<String> selectPermissionCodesByUserId(@Param("userId") Long userId);
// 登录成功后更新
int updateLoginSuccess(@Param("userId") Long userId, @Param("loginIp") String loginIp);
// 注册时使用
int insert(User user);
boolean existsByUsername(@Param("username") String username);
boolean existsByEmail(@Param("email") String email);
// 密码重置
int updatePasswordByEmail(@Param("email") String email, @Param("newPassword") String newPassword);
}
UserMapper.xml
路径 : clx-auth/src/main/resources/mapper/UserMapper.xml
职责: MyBatis SQL 映射文件
登录查询 (第21-27行):
xml
<select id="selectByUsername" resultMap="UserResultMap">
SELECT user_id, username, password, nickname, status, is_deleted
FROM sys_user
WHERE username = #{username}
AND is_deleted = 0
LIMIT 1
</select>
登录成功更新 (第69-76行):
xml
<update id="updateLoginSuccess">
UPDATE sys_user
SET last_login_ip = #{loginIp},
last_login_time = NOW(),
login_count = IFNULL(login_count, 0) + 1
WHERE user_id = #{userId}
AND is_deleted = 0
</update>
用户名唯一性检查 (第110-115行):
xml
<select id="existsByUsername" resultType="boolean">
SELECT COUNT(*) > 0
FROM sys_user
WHERE username = #{username}
AND is_deleted = 0
</select>
3.7 Common 模块
R (统一响应)
路径 : clx-common-core/src/main/java/com/clx/common/core/domain/R.java
职责: 所有 API 接口的统一响应封装
java
@Data
public class R<T> implements Serializable {
public static final int SUCCESS = 200;
public static final int FAIL = 500;
private int code; // 状态码
private String msg; // 消息
private T data; // 数据
private long timestamp; // 时间戳
// 成功静态方法
public static <T> R<T> ok() { return new R<>(SUCCESS, "操作成功", null); }
public static <T> R<T> ok(T data) { return new R<>(SUCCESS, "操作成功", data); }
// 失败静态方法
public static <T> R<T> fail(String msg) { return new R<>(FAIL, msg, null); }
public static <T> R<T> fail(int code, String msg) { return new R<>(code, msg, null); }
// 判断方法
public boolean isSuccess() { return SUCCESS == this.code; }
}
TokenConstants
路径 : clx-common-core/src/main/java/com/clx/common/core/constant/TokenConstants.java
职责: Token 相关常量定义
java
public final class TokenConstants {
// Redis Key 前缀
public static final String ACCESS_TOKEN_KEY = "clx:auth:access:";
public static final String LOGIN_ATTEMPT_KEY = "clx:auth:attempt:";
// 有效期(秒)
public static final long ACCESS_TOKEN_EXPIRATION = 14400L; // 4小时
public static final long ACTIVE_TOKEN_EXPIRATION = 7200L; // 2小时
public static final long REFRESH_TOKEN_EXPIRATION = 604800L; // 7天
// 登录失败锁定
public static final int MAX_LOGIN_ATTEMPT = 5; // 最大尝试次数
public static final long LOGIN_LOCK_TIME = 1800L; // 锁定时间 30分钟
}
SecurityConstants
路径 : clx-common-core/src/main/java/com/clx/common/core/constant/SecurityConstants.java
职责: 安全相关常量
java
public final class SecurityConstants {
public static final String TOKEN_HEADER = "Authorization"; // Token 头名
public static final String TOKEN_PREFIX = "Bearer "; // Token 前缀
public static final String USER_ID_HEADER = "X-User-Id"; // 用户 ID 头
public static final String USERNAME_HEADER = "X-Username"; // 用户名头
public static final String ADMIN_ROLE = "admin"; // 管理员角色
}
AuthException
路径 : clx-common-core/src/main/java/com/clx/common/core/exception/AuthException.java
职责: 认证相关异常,使用工厂方法创建
java
@Getter
public class AuthException extends RuntimeException {
private final int code;
public AuthException(ResponseCode responseCode) {
super(responseCode.getMessage());
this.code = responseCode.getCode();
}
// 静态工厂方法(统一错误码)
public static AuthException loginFailed() {
return new AuthException(ResponseCode.LOGIN_FAILED);
}
public static AuthException captchaError() {
return new AuthException(ResponseCode.CAPTCHA_ERROR);
}
public static AuthException accountLocked() {
return new AuthException(ResponseCode.ACCOUNT_LOCKED);
}
public static AuthException tooManyAttempts() {
return new AuthException(ResponseCode.TOO_MANY_LOGIN_ATTEMPTS);
}
}
3.8 Security 模块
SaTokenConfig
路径 : clx-common-security/src/main/java/com/clx/common/security/config/SaTokenConfig.java
职责: 安全公共配置
BCrypt 密码加密器 (第60-63行):
java
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder(); // 默认 strength=10
}
使用方式:
java
// 加密
String encoded = passwordEncoder.encode("admin123");
// 结果:$2a$10$xxx...(每次不同)
// 校验
boolean match = passwordEncoder.matches("admin123", encoded);
CORS 跨域配置 (第83-113行):
java
@Bean
public CorsFilter corsFilter(...) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(...); // 允许的前端域名
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(allowCredentials);
config.setExposedHeaders(List.of("Authorization")); // 暴露 Token 头给前端
config.setMaxAge(3600L);
return new CorsFilter(source);
}
SaTokenJwtConfig
路径 : clx-common-security/src/main/java/com/clx/common/security/config/SaTokenJwtConfig.java
职责: 切换 sa-Token 为 JWT 模式
java
@Configuration
public class SaTokenJwtConfig {
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple(); // JWT Simple 模式
}
}
模式说明:
- 注册此 Bean 后,
StpUtil.getTokenValue()返回 JWT 格式 Token - Simple 模式:Token 是 JWT,但会话数据仍存 Redis
- 优点:保留踢人下线、权限刷新功能;网关可直接解析 JWT 获取 userId
StpInterfaceImpl
路径 : clx-common-security/src/main/java/com/clx/common/security/config/StpInterfaceImpl.java
职责: sa-Token 权限接口实现(当前返回空列表)
java
public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return Collections.emptyList(); // 后续实现权限查询
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return Collections.emptyList(); // 后续实现角色查询
}
}
后续扩展 : 可注入 UserMapper,调用 selectRoleCodesByUserId 和 selectPermissionCodesByUserId
SaTokenExceptionHandler
路径 : clx-common-security/src/main/java/com/clx/common/security/exception/SaTokenExceptionHandler.java
职责: 统一处理 sa-Token 框架异常
java
@RestControllerAdvice
@Order(-1) // 优先级高于其他异常处理器
public class SaTokenExceptionHandler {
// 未登录异常(Token 无效/过期/被踢出)
@ExceptionHandler(NotLoginException.class)
public ResponseEntity<R<Void>> handleNotLoginException(NotLoginException e) {
return ResponseEntity.status(401).body(R.fail(401, "请先登录"));
}
// 无权限异常(@SaCheckPermission 校验失败)
@ExceptionHandler(NotPermissionException.class)
public ResponseEntity<R<Void>> handleNotPermissionException(NotPermissionException e) {
return ResponseEntity.status(403).body(R.fail(403, "没有权限访问"));
}
// 无角色异常(@SaCheckRole 校验失败)
@ExceptionHandler(NotRoleException.class)
public ResponseEntity<R<Void>> handleNotRoleException(NotRoleException e) {
return ResponseEntity.status(403).body(R.fail(403, "没有权限访问"));
}
}
3.9 验证码服务
CaptchaService
路径 : clx-auth/src/main/java/com/clx/auth/service/CaptchaService.java
职责: 图形验证码生成与校验
java
@Service
public class CaptchaService {
private static final String CODE_CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; // 去除易混淆字符
private static final int CODE_LENGTH = 4;
private static final long CAPTCHA_EXPIRE_SECONDS = 600L; // 10分钟过期
// 生成验证码
public CaptchaResult generateCaptcha() {
String captchaId = UUID.randomUUID().toString();
String code = generateCode(); // 4位随机字符
String captchaImage = generateCaptchaImage(code); // base64 PNG 图片
// 存 Redis
redisTemplate.opsForValue().set("captcha:" + captchaId, code, 600, TimeUnit.SECONDS);
return new CaptchaResult(captchaId, captchaImage);
}
// 校验验证码
public boolean verifyCaptchaCode(String captchaId, String code) {
String storedCode = redisTemplate.opsForValue().get("captcha:" + captchaId);
if (storedCode == null) return false;
boolean valid = storedCode.equalsIgnoreCase(code);
if (valid) redisTemplate.delete("captcha:" + captchaId); // 一次性使用
return valid;
}
// 绘制验证码图片(干扰线、噪点)
private String generateCaptchaImage(String code) { ... }
}
四、登录流程图
┌──────────┐ POST /auth/login ┌──────────────┐
│ 前端 │ ───────────────────────▶│AuthController│
└──────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ AuthService │
│ Impl │
└──────┬───────┘
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│CaptchaService│ │ UserMapper │ │ Redis │
│(验证码校验) │ │(用户查询) │ │(失败计数) │
└────────────┘ └────────────┘ └────────────┘
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│ Redis │ │ MySQL │
│(验证码存储)│ │ sys_user │
└────────────┘ └────────────┘
│
┌──────────────────┴──────────────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ sa-Token │ │ Redis │
│StpUtil.login│ │(JWT Token) │
└────────────┘ └────────────┘
五、API 接口说明
登录接口
请求:
http
POST /auth/login
Content-Type: application/json
{
"username": "admin",
"password": "admin123",
"captchaId": "uuid-xxx",
"captchaCode": "AB3K",
"rememberMe": true
}
响应:
json
{
"code": 200,
"msg": "操作成功",
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"tokenName": "Authorization",
"tokenTimeout": 2592000,
"activeTimeout": 604800,
"rememberMe": true
},
"timestamp": 1713801234567
}
获取当前用户
请求:
http
GET /auth/me
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
响应:
json
{
"code": 200,
"data": {
"userId": 1,
"username": "admin",
"tokenInfo": {
"tokenName": "Authorization",
"tokenValue": "...",
"tokenSessionTimeout": 2591000
}
}
}
登出
请求:
http
POST /auth/logout
Authorization: Bearer xxx
响应:
json
{
"code": 200,
"msg": "操作成功"
}
六、异常处理
| 异常类型 | HTTP 状态码 | 触发场景 |
|---|---|---|
NotLoginException |
401 | Token 无效/过期/被踢出 |
NotPermissionException |
403 | 缺少权限 |
NotRoleException |
403 | 缺少角色 |
SaTokenException |
401 | 其他认证异常 |
七、安全特性
7.1 防暴力破解
- 登录失败 5 次后锁定账户 30 分钟
- 使用 Redis 记录失败次数
- Key 格式:
clx:auth:attempt:{username}
7.2 防时序攻击
用户不存在时也执行密码比对,防止通过响应时间判断用户是否存在:
java
String encodedPassword = user == null ? DUMMY_PASSWORD_HASH : user.getPassword();
return passwordEncoder.matches(rawPassword, encodedPassword) && user != null;
7.3 密码加密
- 使用 BCrypt 算法
- 每次加密生成不同哈希值(内置随机盐)
- 固定 60 字符输出
- 默认 10 轮计算
7.4 验证码保护
- 图形验证码 10 分钟过期
- 验证后立即删除(一次性使用)
- 去除易混淆字符(0/O、1/I/l)
八、有效期说明
| 场景 | 绝对有效期 | 活跃有效期 |
|---|---|---|
| 普通登录 | 4 小时 | 2 小时 |
| 记住我 | 30 天 | 7 天 |
- 绝对有效期: Token 的最大存活时间,无论是否活跃都会过期
- 活跃有效期: 无操作后自动过期时间,每次请求会刷新