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

1.短信登录

1.0 问题记录

1.0.1 redis 重复 token 问题

每次用户登录时,后端会创建一个新的 token 并存入 Redis,但之前登录的 token 还没有过期。这可能会导致以下问题:

  1. 1. Redis 中存在大量未过期但实际已不使用的 token
  2. 2. 同一用户可能有多个有效 token
  3. 3. 资源浪费和潜在的安全风险

原因:

Logout 时会清除 token,但平常关闭服务时并没有点击退出登录导致 token 积累....

1.1 session 实现登录流程

1.1.1 发送验证码:

用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号

如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

1.1.2 短信验证码登录、注册:

用户将验证码和手机号进行输入,后台从 session 中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到 session 中,方便后续获得当前登录信息

1.1.3 校验登录状态:

用户在请求时候,会从 cookie 中携带者 sessionId 到后台,后台通过 sessionId 从 session 中拿到用户信息,如果没有 session 信息,则进行拦截,如果有 session 信息,则将用户信息保存到 threadLocal 中,并且放行

 

 

1.2 登录拦截功能

1.2.1 Tomcat 运行原理

  • 当用户发起请求时,会访问我们像 tomcat 注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat 也不例外
  • 当监听线程知道用户想要和 tomcat 连接连接时,那会由监听线程创建 socket 连接,socket 都是成对出现的
  • 用户通过 socket 像互相传递数据,当 tomcat 端的 socket 接受到数据后,此时监听线程会从 tomcat 的线程池中取出一个线程执行用户请求
  • 在我们的服务部署到 tomcat 后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的 controller,service,dao 中,并且访问对应的 DB
  • 在用户执行完请求后,再统一返回,再找到 tomcat 端的 socket,再将数据写回到用户端的 socket,完成请求和响应

通过以上讲解,我们可以得知 每个用户其实对应都是去找 tomcat 线程池中的一个线程来完成工作的,使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用 threadlocal 来做到线程隔离,每个线程操作自己的一份数据

  1. 请求接收阶段
    • Acceptor 线程:Tomcat 启动时,会创建 1-2 个 Acceptor 线程(非监听线程),负责监听服务器端口(如 8080),接受客户端 TCP 连接请求。
    • Socket 配对:当客户端发起请求时,Acceptor 线程会创建一个 Socket 连接(客户端 Socket 与服务端 Socket 成对出现)。
  2. I/O 事件处理
    • Poller 线程:Acceptor 将连接交给 Poller 线程(基于 NIO 模式),Poller 通过 Selector 监听 Socket 的读写事件(如 HTTP 请求数据到达)。
    • 事件触发:当 Socket 有数据可读时,Poller 线程会将请求封装成任务,提交到工作线程池。
  3. 业务处理阶段
    • Worker 线程池:Tomcat 从线程池中分配一个工作线程(Worker)处理请求:
      1. 解析 HTTP 协议,生成HttpServletRequest和HttpServletResponse对象。
      2. 根据 URL 匹配部署的 Web 应用(Context)、Servlet 路径(Wrapper)。
      3. 依次经过 Filter 链,最终调用目标 Servlet(或 Spring MVC 的 DispatcherServlet)。
      4. 执行业务逻辑(Controller→Service→DAO→DB),生成响应数据。
  4. 响应返回
    • Worker 线程将响应数据写入 Socket 输出流,通过 TCP 返回客户端。
    • 线程释放回线程池,完成一次请求-响应周期。

1.2.2 ThreadLocal

如果小伙伴们看过 threadLocal 的源码,你会发现在 threadLocal 中,无论是他的 put 方法和他的 get 方法,都是先从获得当前用户的线程,然后从线程中取出线程的成员变量 map,只要线程不一样,map 就不一样,所以可以通过这种方式来做到线程隔离

ThreadLocal 的线程隔离机制

  • 线程独立性:每个 HTTP 请求由独立的 Worker 线程处理,线程之间互不干扰。
  • ThreadLocal 原理:
    ThreadLocal 为每个线程维护一个独立的变量副本(通过Thread.currentThread().threadLocals存储),实现线程封闭(Thread Confinement)。
// 示例:存储用户会话信息
ThreadLocal<User> currentUser = new ThreadLocal<>();
currentUser.set(user);  // 当前线程独享
User user = currentUser.get(); // 仅当前线程可获取
  • 典型应用场景:
    • 用户会话(Session)管理(如 Spring 的RequestContextHolder)。
    • 数据库连接隔离(如 MyBatis 的SqlSession绑定线程)。
    • 避免参数透传(如链路追踪的 TraceID)。

关键补充说明

  1. 1. 线程池配置:Tomcat 的线程池大小(maxThreads)直接影响并发能力,需根据业务特点调整。
  2. 2. 连接器(Connector):支持 NIO(非阻塞)、APR(高性能)等模式,默认 NIO 适合大多数场景。
  3. 3. 注意事项:
    • ThreadLocal 需手动remove(),否则可能导致内存泄漏(尤其线程池场景)。
    • Tomcat 的线程模型是同步阻塞的(Servlet 规范),异步处理需使用 AsyncContext 或 Reactive 编程。

1.2.3 登录拦截

 

1.3 session 共享问题

每个 tomcat 中都有一份属于自己的 session,假设用户第一次访问第一台 tomcat,并且把自己的信息存放到第一台服务器的 session 中,但是第二次这个用户访问到了第二台 tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的 session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是 session 拷贝,就是说虽然每个 tomcat 上都有不同的 session,但是每当任意一台服务器的 session 修改时,都会同步给其他的 Tomcat 服务器的 session,这样的话,就可以实现 session 的共享了

但是这种方案具有两个大问题:

  1. 每台服务器中都有完整的一份 session 数据,服务器压力过大。
  2. session 拷贝数据时,可能会出现延迟

所以咱们后来采用的方案都是基于 redis 来完成,我们把 session 换成 redis,redis 数据本身就是共享的,就可以避免 session 共享的问题了

  • 在 Servlet 规范中,Session 是由容器(如 Tomcat)实现的,核心接口是HttpSession。
  • Session 数据默认存储在服务器内存中(ConcurrentHashMap)
  • 每个 Tomcat 实例维护自己的 Session 存储
  • Session ID 通过 Cookie 或 URL 重写与客户端关联
  • 在负载均衡环境下,请求可能被分发到不同服务器
  • 无状态的负载均衡器不知道请求关联的 Session 存储在哪个服务器
  • 即使有 Session ID,其他服务器也没有对应的 Session 数据

1.3.1 Session 的基本概念

Session(会话)是 Web 开发中用于跟踪用户状态的机制,其核心特点是:

  • 服务器端存储:用户数据保存在服务端
  • 客户端关联:通过 Session ID(通常通过 Cookie 或 URL 重写)与客户端绑定
  • 有状态性:记录用户在一段时间内的交互状态

1.3.2 Session 共享问题的本质

在分布式/集群环境下,Session 共享问题源于状态存储位置与请求路由机制之间的矛盾:

  1. 1. 存储位置矛盾:
    • 传统 Session 存储在单个服务器内存中
    • 但集群环境下有多台服务器
  2. 2. 路由机制矛盾:
    • 负载均衡器采用无状态分发(如轮询、随机)
    • 但业务需要有状态识别(同一用户的请求应关联相同 Session)

1.3.3 典型问题场景

场景 1:基础负载均衡

用户A → 请求1 → 负载均衡 → 服务器1(创建Session)→ 请求2 → 负载均衡 → 服务器2(无此Session)

结果:用户需要重新登录,状态丢失

场景 2:服务器宕机

用户A → 服务器1(Session存储地)
服务器1崩溃 → 请求被转到服务器2(无Session数据)

结果:用户会话中断

场景 3:水平扩展

原有3台服务器 → 新增第4台服务器
新请求可能被路由到没有历史Session的新服务器

结果:扩展导致会话一致性被破坏

1.3.4 Session 共享的四大核心挑战

  1. 1. 数据一致性:
    • 如何保证所有服务器看到的 Session 数据一致
    • 特别是并发修改时的数据同步
  2. 2. 实时性要求:
    • Session 变更需要及时传播到所有节点
    • 平衡一致性与性能的关系
  3. 3. 故障恢复:
    • 单个节点故障不应影响整体可用性
    • 新节点应能快速获取已有 Session 数据
  4. 4. 扩展性:
    • 解决方案不应成为系统扩展的瓶颈
    • 支持动态增减节点

1.3.5 主流解决方案对比

方案 1:粘滞会话(Sticky Session)

原理:

  • 负载均衡器通过特定算法(如 IP 哈希)保证同一用户的请求始终路由到同一服务器

优点:

  • 实现简单
  • 无需修改应用代码

缺点:

  • 失去负载均衡的灵活性
  • 节点故障时关联会话丢失
  • 不符合 REST 无状态原则

方案 2:Session 复制

原理:

  • 所有服务器间同步 Session 变更
  • 形成全网状的数据同步

优点:

  • 任意节点都可处理请求
  • 故障转移平滑

缺点:

  • 网络带宽消耗大(O(n²)复杂度)
  • 同步延迟可能导致数据不一致
  • 不适合大规模集群

方案 3:集中式存储

原理:

  • 将会话数据存储在外部集中存储(Redis/Memcached/数据库)
  • 所有服务器访问统一数据源

优点:

  • 真正解决共享问题
  • 良好的扩展性
  • 明确的持久化策略

缺点:

  • 引入外部依赖
  • 网络延迟增加
  • 需要处理缓存失效问题

方案 4:客户端存储

原理:

  • 将会话数据加密后存储在客户端(Cookie/本地存储)
  • 服务端无状态

优点:

  • 完全避免服务端存储
  • 天然支持扩展

缺点:

  • 安全性挑战(需严格加密)
  • 数据大小受限
  • 每次请求需传输完整会话数据

1.3.6 现代架构的最佳实践

  1. 1. 无状态优先原则:
    • 尽可能减少会话中的状态数据
    • 将状态外移到数据库/缓存
  2. 2. 分层会话设计:
    • 高频访问数据:内存缓存
    • 重要数据:持久化存储
    • 临时状态:客户端存储
  3. 3. 混合解决方案:
    graph TD
    A[客户端] --> B[负载均衡]
    B --> C[服务器集群]
    C --> D[集中式Redis存储]
    C --> E[本地内存缓存]
  4. 4. 失效策略:
    • 设置合理的 TTL(Time-To-Live)
    • 主动清理与被动过期结合
    • 考虑分布式锁机制

1.3.7 特殊场景考量

  1. 1. 微服务架构:
    • 每个服务维护自己的"局部会话"
    • 通过 JWT 等机制传递用户上下文
  2. 2. Serverless 环境:
    • 强制无状态设计
    • 依赖外部存储服务
  3. 3. 长连接应用:
    • WebSocket 连接与 HTTP 会话的映射
    • 连接迁移时的状态转移

1.3.8 总结决策树

是否需要会话共享?
├─ 否 → 单机部署或粘滞会话
└─ 是 → 选择集中存储方案├─ 高性能要求 → Redis/Memcached├─ 强一致性要求 → 数据库+缓存└─ 安全敏感 → 客户端存储+加密

理解 Session 共享问题的核心在于认识到有状态服务与无状态扩展之间的本质矛盾。现代分布式系统通常采用"尽量无状态,必要时集中存储"的折中方案,在一致性与可用性之间取得平衡。

1.4 redis 解决共享问题

1.4.1 设计 KEY 的结构

首先我们要思考一下利用 redis 来存储数据,那么到底使用哪种结构呢?

由于存入的数据比较简单,我们可以考虑使用 String,或者是使用哈希,如下图

如果使用 String,同学们注意他的 value,多占用一点空间

如果使用哈希,则他的 value 中只会存储他数据本身,如果不是特别在意内存,其实使用 String 就可以啦。

从优化的角度讲使用 Hash 比较好。

1.4.2 设计 KEY 的细节

所以我们可以使用 String 结构,就是一个简单的 key,value 键值对的方式,但是关于 key 的处理,session 他是每个用户都有自己的 session,但是 redis 的 key 是共享的,咱们就不能使用 code 了

在设计这个 key 的时候,我们之前讲过需要满足两点

  1. 1. key 要具有唯一性
  2. 2. key 要方便携带

如果我们采用 phone:手机号这样的数据来存储当然是可以的,但是如果把这样的敏感数据存储到 redis 中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串 token,然后让前端带来这个 token 就能完成我们的整体逻辑了

1.4.3 整体访问流程

  • 当注册完成后
  • 用户去登录会去校验用户提交的手机号和验证码
    • 是否一致,如果一致,则根据手机号查询用户信息
    • 不存在则新建
    • 最后将用户数据保存到 redis,并且生成 token 作为 redis 的 key
  • 当我们校验用户是否登录时
    • 会去携带着 token 进行访问
    • 从 redis 中取出 token 对应的 value,判断是否存在这个数据
    • 如果没有则拦截,如果存在则将其保存到 threadLocal 中,并且放行

 

1.5 解决状态登录刷新问题

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录 token 令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的

优化方案:

既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了 threadLocal 的数据,所以此时第二个拦截器只需要判断拦截器中的 user 对象是否存在即可,完成整体刷新功能。

RefreshTokenInterceptor:

不管用户访问那个路径都会被拦截,判断 token 是否有效

  • 无效则放行给 LoginInterceptor 拦截器
    • 判断路径是否为必须登录
      • 是则拦截请求,校验 ThredLocal 中是否有信息,无则返回 401,有则放行
      • 不是则放行

       

  • 有效则将信息放入 ThreadLocal 并刷新 token 有效期
  • 在拦截器中配置 order()来控制先后,越小优先级越高

 

1.6 退出登录功能

@PostMapping("/logout")
public Result logout(@RequestHeader(value = "authorization") String token) {return userService.logout(token);
}

 

@Override
public Result logout(String token) {// 1.删除Redis中的tokenString tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.delete(tokenKey);// 2.清除ThreadLocal中的用户UserHolder.removeUser();return Result.ok();
}

1.7 登录业务的实现设计方案

 

登录业务是几乎所有系统都需要的基础功能,根据您的代码和现代 Web 应用的发展,我为您总结几种常见的登录实现设计方案:

 

1. 基于 Session 的登录方案

 

这是传统的登录实现方式:

 

  • 工作原理:
    • 用户登录成功后,服务器创建 Session 并生成 SessionID
    • SessionID 通过 Cookie 返回给浏览器
    • 后续请求会自动携带 Cookie 中的 SessionID
    • 服务器通过 SessionID 识别用户身份

 

  • 优点:
    • 实现简单,Spring 框架原生支持
    • 安全性较高,SessionID 随机生成
    • 服务端可控性强

 

  • 缺点:
    • 分布式系统中 Session 共享问题
    • 服务器需要存储 Session,增加服务器负担
    • 跨域请求处理复杂

 

2. 基于 Token 的登录方案(您当前使用的方案)

 

  • 工作原理:
    • 用户登录成功后,服务器生成 Token
    • Token 返回给客户端存储(如 localStorage)
    • 后续请求在 Header 中携带 Token
    • 服务器验证 Token 有效性

 

  • 优点:
    • 无状态,适合分布式系统
    • 可跨域使用
    • 可以在客户端存储,减轻服务器压力

 

  • 缺点:
    • 需要自行处理 Token 的生成、验证和过期
    • Token 一旦泄露,安全风险较大
    • 无法像 Session 那样方便地实现强制登出

 

3. 基于 JWT(JSON Web Token)的登录方案

 

  • 工作原理:
    • 用户登录成功后,服务器生成 JWT
    • JWT 包含用户信息、过期时间等,并使用密钥签名
    • 客户端存储 JWT 并在请求中携带
    • 服务器验证 JWT 签名和有效期

 

  • 优点:
    • 完全无状态,服务器不需要存储会话信息
    • 包含用户信息,减少数据库查询
    • 支持跨域,适合微服务架构

 

  • 缺点:
    • Token 无法主动失效,只能等过期
    • JWT 体积较大,增加网络传输量
    • 敏感信息不宜放在 JWT 中(虽然签名但内容可解码)

 

4. OAuth 2.0 / 第三方登录

 

  • 工作原理:
    • 利用第三方平台(如微信、QQ、GitHub)的用户系统
    • 用户授权后获取第三方平台提供的 Token
    • 使用 Token 获取用户信息并在本系统创建或关联账号

 

  • 优点:
    • 简化用户注册流程
    • 提高用户体验,无需记忆新密码
    • 可获取第三方平台的用户信息

 

  • 缺点:
    • 依赖第三方平台稳定性
    • 实现复杂度较高
    • 需要处理用户账号关联问题

 

5. 双因素认证 (2FA)

 

  • 工作原理:
    • 除了用户名密码外,还需要第二种验证方式
    • 常见的第二因素包括:短信验证码、邮箱验证码、认证器 App 等
    • 两种因素都验证通过才允许登录

 

  • 优点:
    • 大幅提高安全性
    • 可以防止密码泄露导致的账号被盗
    • 适合金融、支付等高安全需求场景

 

  • 缺点:
    • 增加了用户登录的复杂度
    • 需要额外的基础设施(如短信服务)
    • 可能影响用户体验

 

6. 单点登录 (SSO)

 

  • 工作原理:
    • 用户只需登录一次,就可以访问多个相关系统
    • 通常基于中央认证服务(CAS)或 SAML 协议实现
    • 各子系统通过票据或断言验证用户身份

 

  • 优点:
    • 提升用户体验,避免重复登录
    • 统一的用户管理
    • 适合企业内多系统集成场景

 

  • 缺点:
    • 实现复杂度高
    • 中央认证服务成为单点故障风险
    • 安全设计要求高

 

您当前实现的方案分析

 

您目前使用的是基于 Token + Redis 的登录方案:

 

  1. 1. 用户登录成功后,生成随机 UUID 作为 token
  2. 2. 将用户信息存储在 Redis 中,key 为LOGIN_USER_KEY + token
  3. 3. 设置合理的过期时间
  4. 4. 用户登出时,主动删除 Redis 中的 token

 

这种方案结合了 Session 和 Token 的优点:

  • 像 Token 一样支持分布式系统
  • 像 Session 一样可以主动使 token 失效
  • 使用 Redis 提高了性能
  • 实现相对简单且安全性较高

 

对于您的点评系统来说,这是一个非常合适的选择。如果未来系统规模扩大,可以考虑向 JWT 或 SSO 方向演进。

 

相关文章:

  • 【速通RAG实战:生成】8.智能生成引擎:大模型与Prompt工程黄金指南
  • 【Docker系列】docker inspect查看容器部署位置
  • (剪映)字幕实现卡拉OK效果
  • Java中Comparator排序原理详解
  • https,http1,http2,http3的一些知识
  • SSH终端登录与网络共享
  • vue配置代理解决前端跨域的问题
  • SSH免密登录
  • 【部署满血Deepseek-R1/V3】大型语言模型部署实战:多机多卡DeepSeek-R1配置指南
  • spring boot lunar 农历的三方库引用,获取日期的农历值
  • Linxu实验五——NFS服务器
  • 再度深入理解PLC的输入输出接线
  • 计算机网络:什么是Mesh组网以及都有哪些设备支持Mesh组网?
  • 网页五子棋对战测试报告
  • Backdrops 5.1.8| 每日更新高质量原创壁纸,解锁高级版,去除所有广告
  • Vision Transformer(ViT)
  • 小程序多线程实战
  • Excel里面怎样批量去掉字串包含的标点符号
  • Kotlin 内联函数深度解析:从源码到实践优化
  • 基于 Q-learning 的城市场景无人机三维路径规划算法研究,可以自定义地图,提供完整MATLAB代码
  • 普京提议于15日在土耳其恢复俄乌直接谈判
  • 4月证券私募产品备案量创23个月新高,股票策略占比超六成
  • 视频丨习近平同普京在主观礼台出席红场阅兵式
  • 昆明阳宗海风景名胜区19口井违规抽取地热水,整改后用自来水代替温泉
  • 如此城市|上海老邬:《爱情神话》就是我生活的一部分
  • 美政府被曝下令加强对格陵兰岛间谍活动,丹麦将召见美代办