当前位置: 首页 > news >正文

黑马点评-短信登陆笔记

黑马点评 - 短信登录与 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) 的取舍

维度HashString(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的选择
  • 拦截器的应用逻辑
http://www.jsqmd.com/news/710793/

相关文章:

  • 重构Android界面叙事:从模板使用到设计系统思维的革命
  • 【数据分析页面】
  • 【Python】面向对象之三大特性
  • 20254323 2025-2026-4—27 《Python程序设计》实验三报告 - Moonshot-_
  • Windows Defender完全移除终极指南:一键彻底卸载系统安全组件的完整解决方案
  • 终极指南:MAA明日方舟自动化助手 - 全功能详解与高效配置教程
  • Swin-UNet实战避坑指南:从论文复现到ACDC数据集心脏分割
  • 代码混合文本处理:技术挑战与多语言NLP实践
  • 深度解析NCM文件解密技术:ncmdump工具实战指南与高级应用方案
  • SkVM 深度解析:为 LLM Agent Skills 构建的编译与运行时系统
  • 文本分块策略与预处理
  • 鸿蒙应用如何测试?这两个工具必须掌握!
  • 从零预训练BERT模型的完整指南与实现
  • 2026年降AI工具处理速度对比:哪款工具最快出结果详细横评
  • 硬件指纹保护实战:三分钟掌握EASY-HWID-SPOOFER核心功能
  • 零代码自动化革命:5分钟用taskt告别重复工作,效率提升300%
  • 八大网盘直链下载终极指南:一键获取真实下载地址的完整教程
  • 2026年招牌广告灯箱实力厂商推荐,聚隆运灯箱为何成为连锁品牌首选,赋能商业未来的专业解决方案
  • BotVisibility Checker:基于37项清单的AI友好度网站审计代理
  • 2026 主流 RPA 产品全方位测评:国际厂商与国产信创 RPA 能力对比
  • 跨平台修复引擎:深度解析GMod性能优化技术方案
  • GRANT模型:3D任务调度与空间定位的融合技术
  • 2026年高含量皂苷冻干三七哪个牌子好?大品牌综合评测+选购避坑+血管养护优选指南 - 资讯焦点
  • 那些年我用过的“网红”开源项目
  • 基于确定性图与分层控制的复杂RAG智能体架构设计与实践
  • 2026年北京实测最新榜单:五大GEO服务商技术实力与落地效率综合横评 - GEO优化
  • 2026年有水票和桶押金的送水店微信小程序怎么做?哪家可以做? - 企业数字化改造和转型
  • 2026年食品科学论文降AI工具推荐:食品安全和营养研究部分降AI方案
  • OmenSuperHub:专为惠普OMEN游戏本打造的开源性能控制工具
  • 20252328 2025-2026-2 《Python程序设计》实验三报告