大学生开发社区项目-CLXHXH-登录功能

登录功能架构文档

本文档详细讲解 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.xxx
  • tokenTimeout: 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,调用 selectRoleCodesByUserIdselectPermissionCodesByUserId


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 的最大存活时间,无论是否活跃都会过期
  • 活跃有效期: 无操作后自动过期时间,每次请求会刷新
相关推荐
兩尛2 小时前
struct,union,Class,bitfield各自的作用和区别
java·开发语言
算.子2 小时前
【Spring AI 实战】八、完整 RAG 问答实战:检索 + 重排序 + 生成全链路
java·人工智能·spring
wuminyu2 小时前
专家视角看 Java 字节码与Class 文件格式
java·linux·c语言·jvm·c++
Gauss松鼠会2 小时前
【openGauss】openGauss 磁盘引擎之 ustore
java·服务器·开发语言·前端·数据库·经验分享·gaussdb
lee_curry2 小时前
线程中断,等待,唤醒与ThreadLocal
java·线程·juc·threadlocal·中断
indexsunny2 小时前
互联网大厂Java面试实战:Spring Boot微服务与Kafka消息队列深度解析
java·spring boot·微服务·面试·kafka·消息队列·电商
杨凯凡2 小时前
【019】IO/NIO 概念:Web 开发要掌握到什么程度
java·开发语言·nio
Nicander2 小时前
JDBC PreparedStatement的作用机制
java
MegaDataFlowers3 小时前
解决idea报错不支持发行版本21
java·ide·intellij-idea