前言

项目又又又有问题,浏览器刷新一下就得重新登录,手机和电脑也无法共享状态。作为一个大二学生,只能说完全就是啥也不会,只能一点点地查资料。本文记录我如何把零散的 Session 改成基于 Redis 的 Token 方案,希望给也在自学的同学一点参考。

问题现象:登录态“稍纵即逝”

最开始我直接用 Spring Boot 自带的 HttpSession 保存用户对象:

1
2
3
4
5
6
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request, HttpSession session) {
User user = userService.login(request.getAccount(), request.getPassword());
session.setAttribute("user", user); // 明明存了,但总丢
return Result.success(user);
}

随之暴露了三个痛点:

  1. 前端用 React,刷新页面后 Session 丢失,用户得重新登录(本地两个浏览器窗口就能稳定复现);
  2. 我重启后端服务,所有用户全被踢下线(开发环境重启一次就得重新登录一遍);
  3. 同一账号 PC 登录后,手机端完全感知不到状态(我用同一 Wi-Fi 下的手机访问本地调试地址验证的)。

解决过程:Redis + Token

1. 技术选型

我调研了三种方案:

  • 原生 Session:依赖单机内存,重启或多实例时会混乱;
  • JWT:无状态但无法主动失效,对我现在的需求来说不好控制;
  • Redis + Token:登录后生成 Token,把用户信息放到 Redis,可控可失效,还能扩展到多节点。

综合复杂度和可维护性,我选择第三种。

2. 引入 Redis 依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3. 配置 Redis 连接

1
2
3
4
5
spring:
redis:
host: localhost
port: 6379
database: 0

本地用 Docker 起了一个 Redis,正式环境也能直接复用配置。

4. Token 工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class TokenUtils {

@Autowired
private StringRedisTemplate redisTemplate;

public String generateToken(Long userId) {
return "USER_TOKEN:" + UUID.randomUUID().toString().replace("-", "");
}

public void storeToken(String token, UserDTO user) {
redisTemplate.opsForValue().set(token,
JSON.toJSONString(user),
2,
TimeUnit.HOURS);
}

public UserDTO verifyToken(String token) {
String userJson = redisTemplate.opsForValue().get(token);
if (userJson == null) {
return null;
}
redisTemplate.expire(token, 2, TimeUnit.HOURS);
return JSON.parseObject(userJson, UserDTO.class);
}
}

我在这里刻意用了 UserDTO,把密码哈希等敏感字段去掉,避免 Redis 里存全量对象。虽然暂时还没部署,但趁早把敏感字段剥离掉也能避免后面迁移时手忙脚乱。

5. 改造登录接口

1
2
3
4
5
6
7
8
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
User user = userService.login(request.getAccount(), request.getPassword());
UserDTO safeUser = UserConvertor.toDTO(user);
String token = tokenUtils.generateToken(user.getId());
tokenUtils.storeToken(token, safeUser);
return Result.success(new LoginResponse(token, safeUser));
}

前端拿到 Token 后写入 localStorage,请求时放到 Authorization 头里。现阶段只是我自己在浏览器里调试,但等以后部署到测试环境只需要把基址调一下就能继续沿用。

6. 拦截器校验登录态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class LoginInterceptor implements HandlerInterceptor {

@Autowired
private TokenUtils tokenUtils;

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
throw new BusinessException("未登录");
}
UserDTO user = tokenUtils.verifyToken(token);
if (user == null) {
throw new BusinessException("登录已过期");
}
UserContext.set(user); // ThreadLocal 保存,方便后续获取
return true;
}
}

在 WebMvcConfigurer 里把拦截器注册到需要鉴权的接口即可。

踩坑记录

  • Token 传递方式:最早放在 URL 查询参数,刷新和复制链接都会泄露,最后改成 Authorization 头;
  • Redis 序列化:默认 JdkSerializationRedisSerializer 让 Key 出现乱码,我统一改用 StringRedisTemplate + JSON;
  • 主动下线:verifyToken 失败时直接抛异常,这样用户在别的设备登录会触发旧 Token 失效;
  • 超时续期:每次验证成功后刷新过期时间,用户只要活跃就不会被踢。

总结

这次查了不少资料让我理解了”登录态=如何安全记住用户”。原生 Session 在单机调试还行,但遇到多端、多实例就力不从心,更别说以后上线部署(可能有吧 。Redis + Token 虽然多写了点代码,却解决了刷新、重启、分布式和主动失效的问题。下一篇正常还会继续记录这个项目,也算是督促自己进步了,希望还能坚持写下去。


相关文章

这个用户中心项目系列的其他文章: