前言
项目又又又有问题,浏览器刷新一下就得重新登录,手机和电脑也无法共享状态。作为一个大二学生,只能说完全就是啥也不会,只能一点点地查资料。本文记录我如何把零散的 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); }
|
随之暴露了三个痛点:
- 前端用 React,刷新页面后 Session 丢失,用户得重新登录(本地两个浏览器窗口就能稳定复现);
- 我重启后端服务,所有用户全被踢下线(开发环境重启一次就得重新登录一遍);
- 同一账号 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); return true; } }
|
在 WebMvcConfigurer 里把拦截器注册到需要鉴权的接口即可。
踩坑记录
- Token 传递方式:最早放在 URL 查询参数,刷新和复制链接都会泄露,最后改成
Authorization 头;
- Redis 序列化:默认
JdkSerializationRedisSerializer 让 Key 出现乱码,我统一改用 StringRedisTemplate + JSON;
- 主动下线:
verifyToken 失败时直接抛异常,这样用户在别的设备登录会触发旧 Token 失效;
- 超时续期:每次验证成功后刷新过期时间,用户只要活跃就不会被踢。
总结
这次查了不少资料让我理解了”登录态=如何安全记住用户”。原生 Session 在单机调试还行,但遇到多端、多实例就力不从心,更别说以后上线部署(可能有吧 。Redis + Token 虽然多写了点代码,却解决了刷新、重启、分布式和主动失效的问题。下一篇正常还会继续记录这个项目,也算是督促自己进步了,希望还能坚持写下去。
相关文章
这个用户中心项目系列的其他文章: