4、用户模块 - 注册与登录:JWT鉴权机制、密码加密存储、用户注册接口、用户登录接口、Token刷新机制
用户模块,说白了就是整个抢单系统的「大门」。
我做过好几个抢单类的项目,每次都是先从用户注册登录开始搭。为什么?因为后面的抢单逻辑、订单分配、资金结算,全都依赖这个基础模块。如果用户认证这块有漏洞,整个系统就等于裸奔。
这一章,我们就把用户注册、登录、JWT鉴权、密码加密、Token刷新这些核心环节,一个一个拆开来讲。
4.1 密码加密存储:别再用明文了
先聊密码存储。我记得刚入行那会儿,有个同事直接把用户密码明文存数据库。我当时就问他:「你不怕被脱裤吗?」他一脸懵。后来项目上线第三天,数据库被拖了,用户密码全部泄露。嗯,那哥们当天就离职了。
所以,密码绝对不能明文存。那用什么?BCrypt。这是目前最主流的密码哈希算法。
为什么选BCrypt?
- 内置盐值(Salt),每次哈希结果都不一样
- 可调节计算强度,抗暴力破解
- 比MD5、SHA系列安全得多
代码示例(Java Spring Boot):
// 注册时加密密码
public String encodePassword(String rawPassword) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder.encode(rawPassword);
}
// 登录时校验密码
public boolean checkPassword(String rawPassword, String encodedPassword) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder.matches(rawPassword, encodedPassword);
}
小提示:BCrypt的强度系数我一般设10-12。太高了影响性能,太低了不安全。你自己权衡。
4.2 JWT鉴权机制:无状态才是王道
传统Session方式,说白了就是服务端存一份用户状态。但抢单系统并发高,Session共享就成了噩梦。你想想看,几百台服务器,Session怎么同步?
JWT(JSON Web Token)就解决了这个问题。它是无状态的,服务端不用存任何会话信息。用户登录后,服务端签发一个Token,客户端每次请求带上它就行。
JWT的结构长这样:
// JWT由三部分组成
header.payload.signature
// 示例
eyJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
我习惯把用户ID、角色、过期时间放在Payload里。注意,千万别放密码等敏感信息,因为Payload只是Base64编码,不是加密的。
警告:JWT的Secret Key一定要放在服务端环境变量里,别硬编码在代码中。我曾经见过有人把Secret Key提交到GitHub,结果整个系统被人伪造Token登录了。
4.3 用户注册接口
注册接口的逻辑其实不复杂,但细节很多。我直接贴代码,然后讲重点。
@PostMapping("/register")
public Result<String> register(@RequestBody @Valid RegisterRequest request) {
// 1. 校验用户名是否已存在
if (userService.existsByUsername(request.getUsername())) {
return Result.error("用户名已存在");
}
// 2. 校验邮箱是否已存在
if (userService.existsByEmail(request.getEmail())) {
return Result.error("邮箱已被注册");
}
// 3. 密码加密
String encodedPassword = passwordEncoder.encode(request.getPassword());
// 4. 创建用户
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword(encodedPassword);
user.setCreateTime(LocalDateTime.now());
// 5. 保存
userService.save(user);
return Result.success("注册成功");
}
这里有几个坑,我踩过:
- 用户名和邮箱必须唯一,否则后面登录会乱套
- 密码长度和复杂度校验,前端后端都要做。我见过前端校验了,后端没校验,结果有人直接调接口存了个「1」当密码
- 注册成功后不要直接返回Token,让用户去登录。这样更安全
4.4 用户登录接口
登录接口的核心就是:校验身份,签发Token。
@PostMapping("/login")
public Result<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
// 1. 根据用户名查找用户
User user = userService.findByUsername(request.getUsername());
if (user == null) {
return Result.error("用户名或密码错误");
}
// 2. 校验密码
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
return Result.error("用户名或密码错误");
}
// 3. 生成JWT
String accessToken = jwtUtil.generateAccessToken(user.getId(), user.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(user.getId());
// 4. 返回
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(jwtUtil.getAccessTokenExpiration());
return Result.success(response);
}
经验之谈:登录失败时,不要告诉用户是「用户名不存在」还是「密码错误」。统一返回「用户名或密码错误」,防止攻击者枚举用户名。
4.5 Token刷新机制
AccessToken有效期一般设15-30分钟。为什么这么短?因为一旦泄露,攻击者只有很短的时间窗口可用。
但用户不可能每15分钟登录一次吧?所以我们需要RefreshToken。
RefreshToken有效期长一些,比如7天。它只用来换取新的AccessToken,不参与业务请求。
@PostMapping("/refresh")
public Result<LoginResponse> refresh(@RequestBody RefreshRequest request) {
// 1. 校验RefreshToken
if (!jwtUtil.validateRefreshToken(request.getRefreshToken())) {
return Result.error("RefreshToken已过期,请重新登录");
}
// 2. 从RefreshToken中提取用户ID
Long userId = jwtUtil.getUserIdFromRefreshToken(request.getRefreshToken());
// 3. 生成新的AccessToken和RefreshToken
User user = userService.findById(userId);
String newAccessToken = jwtUtil.generateAccessToken(user.getId(), user.getUsername());
String newRefreshToken = jwtUtil.generateRefreshToken(user.getId());
// 4. 返回
LoginResponse response = new LoginResponse();
response.setAccessToken(newAccessToken);
response.setRefreshToken(newRefreshToken);
response.setExpiresIn(jwtUtil.getAccessTokenExpiration());
return Result.success(response);
}
注意:每次刷新时,同时刷新AccessToken和RefreshToken。这叫「Token轮换」。防止RefreshToken被重复使用。
4.6 整体流程与知识体系
下面这张图,把整个用户注册登录的流程串起来了。我画的时候特意把JWT的生成和校验过程也标了出来,方便你理解。
4.7 避坑指南
最后,我把自己踩过的坑总结一下:
| 坑点 | 后果 | 解决方案 |
|---|---|---|
| 密码明文存储 | 数据库泄露后用户密码全暴露 | 使用BCrypt加密 |
| JWT Secret硬编码 | 源码泄露后Token可伪造 | 放在环境变量或配置中心 |
| AccessToken有效期过长 | 泄露后攻击者长时间可用 | 设15-30分钟,配合RefreshToken |
| 登录失败提示具体原因 | 攻击者可枚举用户名 | 统一返回「用户名或密码错误」 |
| RefreshToken不轮换 | 泄露后可无限刷新 | 每次刷新时重新生成RefreshToken |
嗯,用户模块的核心内容就这些。你把这些搞懂了,后面的抢单逻辑、订单分配,才能放心地往上搭。毕竟,门锁都没装好,谁敢把贵重物品放屋里?