3、用户认证模块(一):JWT原理与集成、用户登录接口设计与实现、密码加密(BCrypt)、Token的生成与校验

好,咱们开始进入用户认证模块。说实话,这是整个OA系统最核心的部分之一。没有认证,系统就像没上锁的大门,谁都能进。今天这一章,我会带你手把手把JWT、BCrypt、登录接口、Token校验这一整套东西串起来。

3.1 JWT 到底是什么?

JWT,全称 JSON Web Token。说白了,它就是一种“令牌”。你拿着这个令牌,系统就知道你是谁,能干什么。

我刚开始接触JWT时,总觉得它很神秘。后来拆开一看,其实就是三段字符串,用点号隔开:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

这三段分别是:

  • Header(头部):声明加密算法和令牌类型
  • Payload(载荷):存放用户信息,比如用户ID、角色、过期时间
  • Signature(签名):防止数据被篡改

你想想看,如果只存Header和Payload,那谁都能伪造。但有了签名,只要密钥不泄露,别人就改不了。这就是JWT安全性的核心。

核心要点:JWT是自包含的。服务器不需要存Session,每次请求带上Token,解析一下就知道是谁。这在前后端分离架构下特别实用。

3.2 为什么用BCrypt做密码加密?

密码不能明文存,这是底线。但用什么算法加密?MD5?SHA?我建议你别用。为什么呢?

MD5和SHA是哈希算法,算得特别快。快反而是缺点——攻击者可以每秒算几亿次,暴力破解你的密码库。我在项目中见过一个案例,对方用彩虹表几分钟就把一批弱密码全解出来了。

BCrypt不一样。它故意设计得很慢,而且可以调节“工作因子”。工作因子越高,算得越慢。比如工作因子设为10,一次BCrypt计算大约需要100毫秒。攻击者想暴力破解?算一个密码就要100毫秒,算一亿个?那得算到猴年马月去。

另外,BCrypt每次生成的哈希值都不一样。同一个密码,两次加密结果不同。这能有效抵御彩虹表攻击。

我的建议:工作因子设10-12比较合适。太低不安全,太高影响用户体验。我一般设12,登录时用户几乎感觉不到延迟。

3.3 用户登录接口设计与实现

好,理论说完了,咱们直接上代码。先看登录接口的设计:

// 登录请求DTO
public class LoginRequest {
    private String username;
    private String password;
    // getter/setter省略
}

// 登录响应DTO
public class LoginResponse {
    private String token;
    private String username;
    private String role;
    // getter/setter省略
}

接口路径:POST /api/auth/login

登录流程其实不复杂:

  1. 接收用户名和密码
  2. 根据用户名查数据库
  3. 用BCrypt校验密码
  4. 校验通过,生成JWT Token
  5. 返回Token给前端

来看核心代码:

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
        // 1. 查用户
        User user = userService.findByUsername(request.getUsername());
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        // 2. 校验密码
        if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        // 3. 生成Token
        String token = jwtTokenProvider.generateToken(user);

        // 4. 返回结果
        LoginResponse response = new LoginResponse();
        response.setToken(token);
        response.setUsername(user.getUsername());
        response.setRole(user.getRole());

        return ResponseEntity.ok(response);
    }
}

嗯,这里要注意一点。BCrypt.checkpw()这个方法,第一个参数是明文密码,第二个参数是数据库里存的哈希值。它会自动从哈希值里提取盐值,然后重新计算比对。你不用自己处理盐,BCrypt全包了。

3.4 Token的生成与校验

JWT的生成和校验,我习惯封装成一个工具类。这样整个项目里统一调用,维护起来方便。

@Component
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expiration}")
    private long expirationMs;

    // 生成Token
    public String generateToken(User user) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationMs);

        return Jwts.builder()
                .setSubject(user.getId().toString())
                .claim("username", user.getUsername())
                .claim("role", user.getRole())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // 从Token中提取用户ID
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }

    // 校验Token是否有效
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

这里有几个关键点:

  • 密钥要保密:放在配置文件里,不要硬编码。我见过有人把密钥写在代码里然后提交到GitHub...那画面太美我不敢看。
  • 过期时间要合理:一般设30分钟到2小时。太短用户老得重新登录,太长不安全。
  • Claims里别放敏感信息:Payload是Base64编码,不是加密。谁拿到Token都能解码看到内容。密码、身份证号这些别放进去。

避坑指南:我曾经在项目里把用户手机号放进了Payload,结果测试时发现Token被截获,手机号直接暴露了。后来赶紧改成只放用户ID和角色。记住,JWT的Payload不是加密的,别放敏感数据。

3.5 完整的认证流程串起来

现在我们把整个流程走一遍:

步骤 操作 说明
1 用户输入用户名密码 前端发送POST请求到 /api/auth/login
2 后端校验密码 用BCrypt.checkpw()比对
3 生成JWT Token 包含用户ID、角色、过期时间
4 返回Token给前端 前端存到localStorage或cookie里
5 后续请求带上Token 放在Authorization请求头里
6 后端校验Token 解析JWT,提取用户信息

你想想看,这套流程下来,服务器不需要存任何Session信息。每个请求都是无状态的,扩展起来特别方便。加机器?直接加,不用考虑Session同步的问题。

3.6 密码加密的数据库设计

最后提一下数据库里密码字段怎么存。我习惯用varchar(60),因为BCrypt生成的哈希值固定60个字符:

CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(60) NOT NULL,
    role VARCHAR(20) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

注册时加密密码:

String encodedPassword = BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));
user.setPassword(encodedPassword);

BCrypt.gensalt(12)里的12就是工作因子。我建议你设12,安全性和性能平衡得比较好。

个人经验:我刚开始做项目时,工作因子设了15,结果注册时用户要等两三秒。后来降到12,基本感觉不到延迟。记住,安全很重要,但用户体验也不能忽视。

好了,这一章的内容就到这里。JWT原理、BCrypt加密、登录接口、Token生成校验,这一整套东西你已经掌握了。下一章我们会讲如何在Spring Security里集成JWT,实现真正的认证拦截。到时候你会看到,今天写的这些代码会怎么跟框架结合起来。