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
登录流程其实不复杂:
- 接收用户名和密码
- 根据用户名查数据库
- 用BCrypt校验密码
- 校验通过,生成JWT Token
- 返回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,实现真正的认证拦截。到时候你会看到,今天写的这些代码会怎么跟框架结合起来。