黑马点评-短信登陆笔记

黑马点评 - 短信登录与 Redis 鉴权

项目:黑马点评 Day1

标签:#Redis <#SpringBoot> <#鉴权> <#拦截器>

关联:<苍穹外卖-JWT登录> Spring Session <ThreadLocal>

一、为什么用 Redis 替代 Session

Session 在分布式下的核心问题

  1. Session 存在单台服务器内存中,分布式部署时,请求可能被负载均衡分发到不同服务器,导致 Session 失效
  2. Session 复制方案的缺陷
    • 每台服务器都存全量 Session,内存浪费
    • 服务器之间同步有延迟,同步期间请求可能读到旧数据或读不到
  3. 安全性 :基于 Cookie 的 SessionId 容易被 CSRF 攻击

Redis 方案的优势

  • 集中存储,所有服务器共享
  • Redis 本身支持高并发、过期机制
  • Token 通过自定义 Header 传输,规避 CSRF

💡 Spring Session + Redis 本质上做的就是同一件事,只是封装层次更高


二、Token 设计

为什么 Token 用随机字符串而不是手机号/userId

核心原则:身份凭证必须不可预测

  • 用手机号当 Token:知道手机号 = 能伪造身份,等于没鉴权
  • 随机 UUID:攻击者无法构造,安全性建立在"猜不到"上
  • 隐私保护是附带的好处,不是主要原因

Token 设计要点

复制代码
String token = UUID.randomUUID().toString(true); // hutool 工具类,去掉横线
String tokenKey = LOGIN_USER_KEY + token;        // login:token:xxx
  • Token 本身就是随机字符串
  • Redis 的 Key = 前缀 + Token,Token 即定位符
  • 一个 Key 同时承担"身份验证"和"用户信息存储"

三、Redis 存储结构选型

Hash vs String(JSON) 的取舍

维度 Hash String(JSON)
修改单字段 HSET 一步搞定 取出→反序列化→改→序列化→存回,5 步
内存占用 更小 更大(JSON 序列化开销)
并发安全 单字段操作原子 易出现<丢失更新>问题
整体读写 多字段需多次操作 一次搞定

结论 :用户信息这种字段会单独更新的场景,用 Hash 更合适

实际操作

复制代码
// 存:UserDTO 转 Map,存为 Hash
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
    CopyOptions.create().setIgnoreNullValue(true)
        .setFieldValueEditor((k, v) -> v.toString()));
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);

// 设置过期
stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES);

⚠️ 注意:StringRedisTemplate 要求 Hash 的 value 必须是 String,所以要用 setFieldValueEditor 把所有字段转成字符串


四、双拦截器设计【重点】

为什么必须拆成两个拦截器

核心问题 :单拦截器只能拦截"需要登录的路径",但用户在浏览不需要登录的路径(如首页)时,Token 不会被刷新,会出现"看着首页 Token 过期了"的糟糕体验。

职责划分

RefreshTokenInterceptor(第一个)
  • 拦截路径:/**所有路径
  • 职责:只刷新,不拦截
  • 流程:
    1. 从 Header 取 Token
    2. Token 不存在 → 直接放行
    3. Token 存在 → 查 Redis → 存 ThreadLocal → 刷新过期时间 → 放行
  • 关键:永远 return true
LoginInterceptor(第二个)
  • 拦截路径:需要登录的业务路径
  • 职责:只判断,不刷新
  • 流程:
    1. 从 ThreadLocal 取用户
    2. 没用户 → 401 拦截
    3. 有用户 → 放行

拦截器执行顺序

通过 order(int) 控制:数字越小越先执行

复制代码
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
    .addPathPatterns("/**").order(0);
registry.addInterceptor(new LoginInterceptor())
    .excludePathPatterns(...).order(1);

🎯 设计精髓:职责分离,第一个负责全局保活,第二个负责局部拦截


五、Token 续期机制

设计思路:模拟 Session 的活跃保活

  • 续期时机:每次请求都续(在 RefreshTokenInterceptor 里)

  • 续期方式重置为 30 分钟,不是累加

  • 续期对象 :对整个 Hash Key 用 EXPIRE 命令

    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

用户体验

  • 活跃用户:Token 永不过期
  • 离开后 30 分钟未操作:自动失效,需重新登录

六、ThreadLocal 与线程安全【重点】

为什么用 ThreadLocal 存用户信息

  • 每个请求由一个线程处理
  • ThreadLocal 让用户信息在当前线程的所有方法中都能访问,避免参数层层传递
  • 线程之间数据隔离

为什么必须 remove ------ 不是优化,是 Bug 修复

问题 1:线程复用导致的串号
  • Tomcat 用<线程池>,线程处理完请求会被复用
  • 线程 T1 处理用户 A,ThreadLocal 存了 A 的信息
  • T1 接着处理用户 B 的请求,没 remove 的话 → B 读到 A 的数据
  • 这是严重的越权漏洞
问题 2:内存泄漏
  • 涉及Java 引用类型:<强引用>、<弱引用>
  • ThreadLocalMap 的 Entry:key 是弱引用,value 是强引用
  • 线程池的线程长期存活 → ThreadLocalMap 长期存活 → value 强引用的对象永远回收不掉
  • 高并发下持续累积 → 老年代占满 → Full GC 频繁 → OOM

正确写法

复制代码
@Override
public void afterCompletion(...) {
    UserHolder.removeUser(); // 必须!
}

七、关键概念延伸

内存泄漏 vs 内存占用大

  • 内存泄漏:不再需要的对象,由于引用关系无法 GC 回收
  • 内存占用大:对象还在被使用,正常现象
  • OOM:泄漏积累的最终结果

强引用 vs 弱引用

复制代码
User user = new User();              // 强引用:拦着不让 GC
WeakReference<User> ref = new WeakReference<>(user); // 弱引用:拦不住 GC
user = null;
// 此时 GC 一来,User 对象就被回收了

八、整体流程图

复制代码
登录流程:
用户输入手机号 → 发送验证码(存 Redis: login:code:phone)
           ↓
用户提交验证码 → 校验 → 查/建 user → 生成 token → 用户信息存 Redis Hash
           ↓
返回 token 给前端 → 前端存到 sessionStorage

请求流程:
前端请求带 Header: authorization=token
           ↓
RefreshTokenInterceptor: 查 Redis → 存 ThreadLocal → 续期
           ↓
LoginInterceptor: 检查 ThreadLocal 有没有用户
           ↓
Controller: UserHolder.getUser() 拿当前用户
           ↓
afterCompletion: ThreadLocal.remove()

九、面试高频追问

  • Session 共享有几种方案?各自优劣?
  • 为什么不用 JWT?JWT 和 Token+Redis 的区别?
  • 如何实现"踢下线"功能?
  • 多端登录如何处理?
  • Token 被盗用怎么办?
  • ThreadLocal 的 InheritableThreadLocal 是什么?
  • 父子线程间如何传递 ThreadLocal?

十、 自己踩过的坑

  • session 与 redis的选择
  • 拦截器的应用逻辑
相关推荐
Shea的笔记本2 小时前
MindSpore实战笔记:ResNet50中药炮制饮片质量判断复现全记录
笔记
YaBingSec2 小时前
玄机靶场—Apache-druid(CVE-2021-25646) WP
java·开发语言·笔记·安全·php·apache
Hello--_--World2 小时前
React:状态管理 官网笔记
前端·笔记·react.js
苦 涩2 小时前
考研408笔记之操作系统(五)——输入输出(IO)管理
笔记·操作系统·考研408
他是龙5512 小时前
DVWA SQL 注入全级别通关笔记(Low / Medium / High / Impossible)
数据库·笔记·sql
咸鱼翻身小阿橙3 小时前
C++ 与 QML 交互入门笔记
c++·笔记·交互
三品吉他手会点灯3 小时前
STM32F103 学习笔记-21-串口通信(第4节)—串口发送和接收代码讲解(下)
笔记·stm32·单片机·嵌入式硬件·学习
南境十里·墨染春水3 小时前
C++ 笔记 ——STL deque
开发语言·c++·笔记
U盘失踪了3 小时前
学习记录:requests Django登录测试脚本(解决CSRF、重定向问题)
笔记·python·学习·django·csrf