Spring Boot + Redis 实现登录:从 Session 到 Token 的完整方案

发布时间:2026/10/16 11:26:45
Spring Boot + Redis 实现登录:从 Session 到 Token 的完整方案 这套点评实战项目我前后做了好几轮登录模块是第一个被推倒重来又彻底定稿的部分。很多刚上手 Spring Boot 的朋友都会问登录不就是校验用户名密码然后塞 Session 吗干嘛非得引入 Redis等真把项目部署成多实例、把前端换成小程序和 App、再被测试反馈换台设备登录态就丢了请求头里 Cookie 根本带不过来的时候才明白 Session 这套方案在今天的主流项目里有多别扭。这篇文章把基于 Redis 实现登录的完整思路、核心代码和真实踩坑记录全放出来当成这个系列的第一篇后面几篇我们接着聊缓存穿透、分布式锁和热点数据优化。这套登录方案不挑语言思路我用 Java Spring Boot 实现核心逻辑就是把用户登录态从服务器内存搬到 Redis用 Token 代替 Session ID所有依赖登录的接口通过拦截器统一校验。适合正在做单体应用但准备横向扩容的同学也适合想搞清楚为什么要用 Redis 存登录态的人。读完你不仅能照着抄还能明白每行配置为什么这么写。1. 先想清楚登录方案为什么要动 Redis1.1 传统 Session 在真实项目里的四个痛点教科书里把 Session 讲得很简单用户登录成功服务器把用户信息存进内存生成一个 Session ID 通过 Cookie 返回给浏览器下次请求带上 Cookie服务器比对一下 ID 就能认出你是谁。这套流程在单机开发环境里跑得飞快但一旦放到真实项目问题一个接一个蹦出来。第一个痛点是多实例部署。只要项目上了两台服务器负载均衡一转发用户第一次请求落在 A 节点第二次请求落在 B 节点B 节点内存里根本没有这个 Session用户就被强制登出了。解决方案不是没有Session 粘滞、Session 复制都有人用但粘滞会把负载均衡的能力废掉一大半复制又会在节点多的时候产生大量冗余数据越用越难受。第二个痛点是跨域和移动端适配。Web 项目还好Cookie 是浏览器原生支持的但小程序、App 这类客户端对 Cookie 的支持并不可控很多 HTTP 客户端根本不会自动维护 Cookie 会话。做前后端分离更是如此接口域名和前端域名不一样还得专门处理 CORS 里的 Credentials一个没配好Cookie 就带不过来了。第三个痛点是内存占用。Session 默认存服务器内存用户越多内存被吃掉的越多而且很多框架的 Session 还要做持久化重启不丢这又引入了额外的存储和序列化成本。我见过一个项目从没配过 Session 清理内存里堆了一大堆几百年没活跃过的用户会话最后只能靠重启暂时缓解。第四个痛点是被动失效难。你想让某个用户下线 Session 方案里你得知道他对应哪个 Session ID还得找到那台存他 Session 的服务器想想就头大。想做一个账号只能在一处登录管理员强制踢人这类运营需求 Session 方案实现起来非常绕。1.2 Redis 为什么适合做登录态能过期、能共享、能精确删除Session 的痛点Redis 几乎是为解决它们而生的。先说共享。Redis 是独立部署的服务业务服务器无论有多少台节点访问的都是同一个 Redis登录态天然就是全局共享的。扩容时新加节点不用做任何会话同步因为会话根本不在节点上把服务发现有问题的同学直接解放。再说过期。Redis 的 key 支持设置 TTL到期自动删除这跟会话的生命周期太契合了。用户登录态可以设置比如 30 分钟过期用户在活跃时每次请求都刷新这个 TTL就实现了滑动过期。用户不活跃了TTL 到了Redis 自动把数据清掉不需要开发手写定时任务去扫。然后是精确删除。一个 DEL 命令就能把某个用户的登录态立刻干掉实现退出登录踢人下线顶号登录都非常直接。Session 方案里难以实现的运营需求Redis 方案下就是一条命令的事。最后可以对比一下同样是无状态登录的 JWT 方案。JWT 把用户信息加密进客户端保存的令牌里服务端不存任何状态天然适合分布式。但它有个硬伤你没法主动让一个已签发的 JWT 失效。用户被盗号了你想把他踢下线做不到只能等 token 自己过期。想实现顶号登录得额外引入黑名单机制那本质上又是在服务端存状态了既然这样还不如直接用 Redis 存登录态一切尽在掌握。这就是我最终选择 Redis 的核心原因可共享、可过期、可主动删除三个特性正好补齐 Session 和 JWT 各自的短板。2. 模块拆解登录在点评项目里到底做了哪几件事2.1 三条主线流程发码、登录、校验登录模块不是只有一个登录接口那么简单。我把它拆成了三个流程每个流程面临的 Redis 操作重点都不一样。第一个流程是发送验证码。用户输入手机号点击获取验证码后端要做的不是立刻发短信而是一连串防御动作先校验手机号格式对不对再看这个手机号是不是刚刚发过防止用户狂点按钮把短信接口刷爆然后生成一个 6 位随机验证码存进 Redis 并设置过期时间最后才真正调用短信服务商把验证码发出去。在这个流程里Redis 承担的是频控 存储两个职责。第二个流程是登录。用户输入手机号和验证码后端从 Redis 里把之前存的验证码取出来比对比对正确、删掉这个 key验证码一次性用完即焚然后去用户表查这个手机号有没有注册过没有就自动创建一个新用户最后生成一个 Token把用户的核心信息存进 Redis设置 TTL再把 Token 返回给前端。这个流程里Redis 承担的是存储登录态的职责。第三个流程是登录态校验。前端在后续所有需要登录的请求头里带上 Token后端通过拦截器统一处理拿到 Token去 Redis 查有没有对应的用户查到了就放行查不到或者过期了就返回 401。同时还要做一件事——刷新 Token 的 TTL让活跃用户的登录态自动续期。这个流程里Redis 承担的是鉴权 续期两个职责。三个流程看起来简单但每个环节都有细节下面用 Redis 的数据结构设计把这些细节串起来。2.2 key 设计与 TTL 管理怎么设计才能不串号、不占内存Redis 是个大字典所有人共用同一片命名空间key 设计不好就等着数据互相覆盖或者排查到崩溃。我在这个项目里统一用业务前缀 冒号 唯一标识的格式。前缀用英文单词加模块名比如验证码用login:code登录态用login:token登录态所属用户如果需要反向索引再用login:user。冒号不是随便加的Redis 官方推荐这种风格它在可视化工具里会自动按前缀折叠分组排查问题的时候一眼就能看到某个业务下有哪些 key。具体到登录场景我用了三个核心 keykeyvalueTTL用途login:code:{手机号}6 位随机数字验证码5 分钟存验证码过期作废login:token:{token}用户信息的 JSON 字符串30 分钟存登录态滑动续期login:user:{用户id}token 字符串30 分钟反查登录态实现顶号验证码设 5 分钟是行业通行做法太短用户收个短信再看一眼就不够用了太长又给暴力破解留了太多时间窗口。登录态设 30 分钟配合每次请求刷新 TTL用户只要保持在用状态就永远不掉线真正超过 30 分钟完全没活跃才需要重新登录大多数场景下这个配置是合理的。需要更长时长的业务比如记住我这类功能可以单独给另一个 key 设置更长 TTL不要试图用一个 key 兼顾所有场景。TTL 不是越长越好Redis 虽然会自动清理过期 key但大量长期不过期的登录态堆在内存里最终会触发内存淘汰策略到时候就不是你想保留哪个的问题了。login:user:{用户id}这个反向索引很多人会忽略但它是实现顶号登录的关键。用户在新设备上登录按用户 id 把旧的 token 对应的 key 删掉再存新 token就能保证同一时刻只有一个设备在线。如果产品需求是多端在线这个 key 就不需要了做成 token 列表也行。需求决定设计别一上来就把所有 key 都建了。3. 实操从零到一实现 Redis 登录3.1 序列化方案这一步做错登录态直接乱码真正开始写代码前我建议先花五分钟把 Redis 的序列化方案定下来。Spring Boot 里访问 Redis 有两大工具RedisTemplate和StringRedisTemplate。很多初学者上来就用RedisTemplate结果往 Redis 里一存控制台看到的是一串\xAC\xED\x00\x05t...之类的乱码这就是 JDK 默认序列化搞的鬼。JDK 序列化有三个毛病存进去的内容人类完全读不懂排查问题极其痛苦序列化后的体积特别大浪费内存而且 JDK 序列化的数据只有 Java 程序能读将来如果引入 PHP、Go 写的服务想共用数据直接傻眼。我的选择很简单统一用StringRedisTemplatevalue 直接存 JSON 字符串。key 本来就是字符串value 用 JSON 序列化后的字符串Redis 里看到的就是干干净净的明文哪个 key 对应什么业务一目了然。配合项目里已有的 Jackson用户对象转成 JSON 再存取出时再反序列化回对象全程不碰 JDK 序列化。这里有一个容易踩的坑如果用户对象里有LocalDateTime这种 Java 8 时间类型直接用 Jackson 转 JSON 会报错或者转成数组。需要引入jackson-datatype-jsr310模块然后给ObjectMapper注册JavaTimeModule再把writeDatesAsTimestamps关掉。这个坑不下三次在我手底下出现过提前写进配置能省半天排查时间。Configuration public class RedisConfig { Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) { StringRedisTemplate template new StringRedisTemplate(); template.setConnectionFactory(factory); return template; } Bean public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } }3.2 发验证码接口频控、校验、倒计时一次搞定发送验证码接口的核心不是发短信而是怎么挡掉无效请求。我完整走一遍这个接口的逻辑。第一步校验手机号格式。手机号这关不过后面全都不用执行。正则表达式校验第一位是 1第二位是 3-9后面 9 位数字基本覆盖国内手机号格式。这一步可以省掉一次 Redis 查询和一次短信费用值得认真写。第二步查 Redis 里有没有login:code:{手机号}这个 key。如果存在说明 60 秒内才刚刚发过验证码直接提示发送过于频繁连短信都不必调。这就是倒计时的实现原理倒计时不是前端渲染的而是后端根据 Redis 里 key 的 TTL 剩多少返回给前端计算的。前端做 60 秒倒计时只是交互展示后端的 Redis 判断才是真正的护栏。第三步生成 6 位随机验证码。我用了String.format(%06d, random.nextInt(1000000))注意补齐前导零比如生成的是004728格式化后丢不了 0。有人喜欢用 UUID 截一段当验证码没必要6 位数字对用户最友好暴力破解风险靠 TTL 和频控已经压下来了。第四步写 Redis设置 5 分钟 TTL。这里再叠加一个每日发送上限的 key比如login:code:limit:{手机号}每发一次就incr一次第一次发送时设置expire到当天 24 点超过比如 10 次就拒绝发送。这个设计能防住拿别人手机号恶意刷短信的脚本上线当天就被我用上了不然短信费一天比一天离谱。PostMapping(/code) public Result sendCode(RequestParam(phone) String phone) { // 1. 校验手机号格式 if (!RegexUtils.isPhoneInvalid(phone)) { return Result.fail(手机号格式错误); } // 2. 检查 60 秒内是否已经发过 String codeKey login:code: phone; if (stringRedisTemplate.hasKey(codeKey)) { Long ttl stringRedisTemplate.getExpire(codeKey, TimeUnit.SECONDS); return Result.fail(发送过于频繁请 ttl 秒后重试); } // 3. 生成 6 位随机验证码 String code String.format(%06d, new Random().nextInt(1000000)); // 4. 存入 Redis5 分钟有效 stringRedisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES); // 5. 记录每日发送次数 String limitKey login:code:limit: phone; Long count stringRedisTemplate.opsForValue().increment(limitKey); if (count ! null count 1) { stringRedisTemplate.expire(limitKey, Duration.between(LocalDateTime.now(), LocalDate.now().plusDays(1).atStartOfDay()).toMinutes(), TimeUnit.MINUTES); } // 6. 模拟调用短信服务商真实项目中替换成具体 SDK log.info(验证码{}, code); return Result.ok(); }有一个细节要注意第 2 步用hasKey判断弹出的提示语最好做成返回剩余秒数而不是固定写1 分钟内只能发一次。用户体验上差别很大而且实现上只是多一行getExpire的事。3.3 登录接口验证码校验、新用户自动注册、Token 生成登录接口的核心是三个动作验证码比对、用户查询/创建、Token 生成与登录态落库。验证码比对有个关键点先判断 key 是否存在再比对值最后无论对错都要删掉这个 key。删掉的逻辑很多人会漏验证码是一次性的不能允许同一个验证码被反复提交。哪怕用户输错了也要把旧验证码删掉让他重新获取这种用完即焚的习惯能挡住大部分验证码重放攻击。比对的时候我建议用字符串的equals别用有人在这里踩过坑拿反序列化的对象去比较结果莫名的 NPE。用户查询与创建查用户表用手机号查查不到说明是新用户自动注册一个初始昵称给个默认值、头像给个默认图这些生成逻辑在真实项目里也是小小的产品决策先别管花哨的机制能跑通流程最重要。Token 生成有人用 UUID有人用 Hutool 的IdUtil.simpleUUID()还有人用雪花算法。登录态 Token 不需要可读性要的是随机性和唯一性UUID 足够了。注意生成后要替换掉中间的横杠省几个字节是一回事主要在 URL 和请求头里裸奔的时候带横杠的字符串也没啥大问题但干净一点总没坏处。登录态落库把用户 ID、手机号、昵称这些必要信息塞进一个UserDTO转成 JSON 存到login:token:{token}这个 key 里TTL 30 分钟。再次强调不要把密码、密保问题答案这类敏感字段放进这个 JSON登录态里只需要鉴权必需的信息。你存得越少泄露之后损失越小。这个习惯我是在一次线上数据被拖库后养成的当年登录态里塞了明文手机号和个人简介教训相当深刻。顶号逻辑这里提一下。如果你要做一个账号同时只能一个设备登录把新 Token 存进去之后顺手从反向 keylogin:user:{用户id}里取出旧 Token把旧的login:token:{旧token}删掉再写入新的反向索引。这样用户一登录新设备老设备立刻失效实现顶号。PostMapping(/login) public Result login(RequestParam(phone) String phone, RequestParam(code) String code) { String codeKey login:code: phone; String savedCode stringRedisTemplate.opsForValue().get(codeKey); if (savedCode null || !savedCode.equals(code)) { return Result.fail(验证码错误或已过期); } // 验证码一次性删掉防重放 stringRedisTemplate.delete(codeKey); // 查用户不存在则自动注册 User user userService.lambdaQuery().eq(User::getPhone, phone).one(); if (user null) { user createUserWithDefaultProfile(phone); } // 生成 Token保存登录态 String token UUID.randomUUID().toString().replace(-, ); UserDTO userDTO BeanUtil.copyProperties(user, UserDTO.class); String tokenKey login:token: token; stringRedisTemplate.opsForValue().set(tokenKey, objectMapper.writeValueAsString(userDTO), 30, TimeUnit.MINUTES); // 反向索引用于顶号登录 String reverseKey login:user: user.getId(); String oldToken stringRedisTemplate.opsForValue().get(reverseKey); if (oldToken ! null) { stringRedisTemplate.delete(login:token: oldToken); } stringRedisTemplate.opsForValue().set(reverseKey, token, 30, TimeUnit.MINUTES); return Result.ok(token); }登录接口里还有一个容易被忽略的环节把UserDTO转 JSON 时抛出的异常。运营反馈为什么登录偶发失败排查到最后是对象里有字段序列化不成功。所以我在实际项目里都会把 JSON 转换单独抽一个方法try-catch 包住失败了记日志避免异常一路抛到前端返回一个 500用户完全看不懂发生了什么。3.4 登录拦截器一劳永逸的登录态校验与刷新登录态存好了总不能每个需要登录的接口都写一遍查 Redis、解析用户吧。用拦截器统一处理这是这个模块的核心价值所在。先写业务拦截器。preHandle里做三件事。第一从请求头authorization里取出 Token注意大小写我见过前端传Authorization、后端取authorization导致全部 401 的冤案建议前后端约定同一种写法第二拿着 Token 去查 Redis查不到说明登录态过期或者被顶号直接返回 401这里一定要response.setStatus(401)不要只返回一个 JSON 里带 code 的错误前后端分离的架构里HTTP 状态码也是协议的一部分第三查到了就把这个 key 的 TTL 续期到 30 分钟再把UserDTO放进去ThreadLocal方便后面的业务代码直接拿当前用户不用在每个 Controller 方法里定义多余的形参。public class LoginInterceptor implements HandlerInterceptor { private final StringRedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } String key login:token: token; String userJson stringRedisTemplate.opsForValue().get(key); if (userJson null) { response.setStatus(401); return false; } UserDTO userDTO objectMapper.readValue(userJson, UserDTO.class); // 滑动续期 stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES); UserHolder.saveUser(userDTO); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserHolder.removeUser(); } }ThreadLocal的使用有个铁律用完必须清理。afterCompletion里调用removeUser()这一步不能省。Tomcat 的工作线程是复用的线程处理完一个请求不会销毁不清ThreadLocal下一个请求就会读到上一个用户的信息轻则数据串号重则越权访问。这个坑我栽过上线第一天就收到了用户 A 看到了用户 B 数据的工单排查半天才定位到是ThreadLocal没清。最后配置拦截器。实现WebMvcConfigurer把拦截器注册进去并设置放行路径。登录相关接口、静态资源、错误页要放行其余/**全部拦截。很多人这里会犯一个错把WebMvcConfigurer写成了普通的Component而没有继承任何东西或者忘记加Configuration结果拦截器悄悄失效。另一个容易忽略的是路径匹配如果你用 Spring Boot 3.xant_path_matcher的配置有时候需要手动开否则/**不生效所有请求都被拦下来一片红。Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)) .addPathPatterns(/**) .excludePathPatterns(/login, /code, /error); } }4. 上线前会踩的坑实测问题与排查记录4.1 拦截器明明写了却不生效项目里第一个拦路虎就是拦截器失效。日志里能看到LoginInterceptor被 Spring 加载了但请求一个都不拦全部直接进了 Controller。排查顺序我建议固定为三层。第一层看有没有配置类真被扫描到Configuration注解的类所在的包必须能被ComponentScan扫到否则配置类等于没写。第二层看是不是实现了WebMvcConfigurerSpring Boot 3 以前还有一种继承WebMvcConfigurationSupport的写法这个写法会把 Spring Boot 自动配置的 MVC 全部覆盖掉静态资源映射、消息转换器全部失效能不碰就别碰。第三层看拦截器注册时addPathPatterns(/**)的写法在 Spring Boot 2.6 之后路径匹配策略变了部分情况下要用PathMatchConfigurer把匹配策略切回AntPathMatcher否则/**匹配不到任何路径。三层排查法走下来绝大多数拦截器不生效都能解决。4.2 Token 拿到手却查不到用户序列化格式不一致有一次前端反馈某些用户明明刚登录成功下一次请求就 401。追踪发现login:token:对应的 key 确实存在而且 TTL 还有二十多分钟但StringRedisTemplate.opsForValue().get(key)返回了null。问题出在存的时候和取的时候用了两套序列化。登录接口里用了StringRedisTemplate存了一个 JSON 字符串但后来拦截器里图方便用了RedisTemplate去取RedisTemplate默认 JDK 序列化它用 JDK 序列化的方式去解析一个 JSON 字符串自然解析得七零八落取出来是null或者解析异常。这一类的诡异问题直接统一所有操作 Redis 的地方都用StringRedisTemplate就不会发生。这也是我一直强调定方案要趁早的原因前后端接口字段统一用驼峰、Date 用时间戳还是字符串Redis 序列化用 JSON 还是用别的这类全局约定一定是在写第一行代码之前就拍死的。4.3 验证码接口被脚本刷爆这个坑是我在生产环境真实撞见的。上线第二天监控发现短信接口的调用量是昨天同期的三倍打开后台一看全是同一个 IP 在狂点获取验证码显然是有人拿脚本在刷。光靠60 秒发一次根本挡不住脚本可以随机换手机号每次都是新手机号就绕过了单个手机号的频率限制。治本办法是把频率限制从手机号维度扩展到设备/IP 维度和全局配额维度。Redis 里用login:code:ip:{ip}统计每个 IP 每日发送量超过阈值就拒绝再加一个全局计数器比如整个系统每天最多发多少条验证码量到了就该人工介入审查了。短信服务商那边也要配限额万一 Redis 数据被清、后端的判断全部失效服务商的配额还能兜底这是最后一道防线。4.4 ThreadLocal 用户串号前面提到过这里再展开一次。用户 A 登录后看到了用户 B 的订单数据排查过程十分诡异两个用户是不同手机号、不同账号Redis 里的数据也没串但 Controller 拿到的用户却是 B。最后定位在ThreadLocalTomcat 的线程池复用了工作线程前一个请求是 B处理完后没有清掉ThreadLocal下一个请求虽然拦截器正确地从 Redis 拿到了 A 的用户信息但业务代码因为有缓存直接读了旧的ThreadLocal于是 A 就看到了 B 的数据。解决办法已经写在了拦截器代码里afterCompletion一定要清理。按我的习惯凡是往ThreadLocal里写了东西的都得像对待InputStream一样强制在finally里清理一步都不能少。现象可能原因排查方法解决方案所有请求 401拦截器放行配置错登录接口也被拦看日志是否有LoginInterceptor在 excludePathPatterns 加/login登录态随机丢失存和取用了不同 RedisTemplate打印 key 的 value 看是否乱码统一用 StringRedisTemplate验证码同一秒被刷检查逻辑里没有先查再发的原子操作看 Redis key 的 TTL用setIfAbsent加分布式锁产生了大量永久 key登录 key 没设置 TTLredis-cli --bigkeys统计所有 key 都要带 TTL用户数据串号ThreadLocal 未清理在 afterCompletion 打印日志强制调用 removeUser4.5 顶号登录的边界情况顶号登录逻辑看起来简单但有几个边界情况很考验脑筋。一是同一个用户在极短的时间内两次登录第一次登录写入 token1第二次登录时删 token1、写入 token2如果第一次的请求比第二次晚到达就会出现 token1 已经被删了但第一次请求的校验还能通过短暂地把 token1 又写回来了。解决思路是删旧 token 前先判断反向索引里的 token 是不是等于当前请求要删的 token不一致就不删。二是用户 A 在设备 1 登录被盗号者拿去设备 2 登录顶号后设备 1 确实失效了但 Redis 里可能残留设备 1 创建的其他业务数据这些数据要设计生命周期不能全部绑定在登录态上。三是顶号策略本身要看产品需求是想让新设备顶掉老设备还是干脆禁止重复登录这直接决定了反向索引写不写别上来就照抄。5. 上线前最后再检查这几件事代码写完了不代表能上线我这套登录模块上线前反复检查的点整理成一份清单照着走一遍能省很多半夜拦截器报警的功夫。第一个检查 Redis 的配置。连接池超时时间、最大连接数、空闲淘汰参数都要根据业务预估量提前估算。尤其要注意 Redis 的密码和网络安全组配置不要让 Redis 裸奔在公网我见过不止一个团队把 Redis 端口暴露到公网数据库被清空的事件做技术的人一定要有这个敏感度。第二个检查 TTL 策略。确认所有业务 key 都带了合理的过期时间别有不带 TTL 的野 key 长期占用内存。上线前可以写个脚本统计一下 key 的数量和分布心里先有个底。第三个检查异常处理链路。Redis 故障了怎么办这个问题值得想清楚。登录接口在 Redis 不可用时应该快速失败并返回友好提示而不是让用户在转圈圈。有的业务对可用性要求极高会把 Redis 做成主从加哨兵或者干脆用集群模式但那是架构层面的事了至少要在代码里做好降级比如缓存 Redis 不可用标志走临时白名单避免雪崩。第四个检查日志。登录这种高频操作日志一定要有但绝不能冗长。打印用户 id、token 前几位、登录结果和耗时不打印验证码明文、不打印完整 token这是安全和排障的平衡点。第五个检查压测。验证码接口和登录接口是典型的高并发写入场景用压测工具灌一波看看 Redis 的 CPU、内存和响应时间别抱侥幸心理该提前发现的问题一定要压出来。最后再分享一个小经验。登录模块是整个系统的地基地基歪了后面全是歪的。我见过很多项目组上来就怼各种高大上的框架结果一顿操作猛如虎一问登录态怎么存的答曰用的默认的 Session。不是说 Sesseion 不行而是你得知道你选的方案在将来项目变大时会不会成为瓶颈。我在这个点评项目里选择了 Redis 存登录态三台实例随意扩缩容登录模块从头到尾没出过状态相关的事故这个方案经受住了验证。接下来这个系列要继续讲缓存穿透、热点 key 和分布式锁都是登录做完之后紧接着要面对的真实问题回头见。