<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>验证码 on XEDCZQ的博客</title><link>https://xedczq.cn/tags/%E9%AA%8C%E8%AF%81%E7%A0%81/</link><description>Recent content in 验证码 on XEDCZQ的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://xedczq.cn/tags/%E9%AA%8C%E8%AF%81%E7%A0%81/index.xml" rel="self" type="application/rss+xml"/><item><title>Web 登录验证码规范：从老后台排查到 Redis 一次性校验</title><link>https://xedczq.cn/post/web_logincaptchaspec/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0800</pubDate><guid>https://xedczq.cn/post/web_logincaptchaspec/</guid><description>&lt;h1 id="web-登录验证码规范从老后台排查到-redis-一次性校验"&gt;&lt;a href="#web-%e7%99%bb%e5%bd%95%e9%aa%8c%e8%af%81%e7%a0%81%e8%a7%84%e8%8c%83%e4%bb%8e%e8%80%81%e5%90%8e%e5%8f%b0%e6%8e%92%e6%9f%a5%e5%88%b0-redis-%e4%b8%80%e6%ac%a1%e6%80%a7%e6%a0%a1%e9%aa%8c" class="header-anchor"&gt;&lt;/a&gt;Web 登录验证码规范：从老后台排查到 Redis 一次性校验
&lt;/h1&gt;&lt;p&gt;这篇笔记记录我最近围绕 Web 管理端登录验证码做的一次完整排查和改造。事情的起点很简单：我想确认老服务器上的后台验证码到底是不是“真的在校验”，以及新版 &lt;code&gt;web_3&lt;/code&gt; 应该把验证码流程收敛成什么样，才算一套真正完整的登录安全链路。&lt;/p&gt;
&lt;p&gt;这次我主要做了三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排查老服务器 &lt;code&gt;yzdwx.yizhoudao.net/admin/&lt;/code&gt; 的验证码真实逻辑&lt;/li&gt;
&lt;li&gt;确认老 Web 是否真的启用了 Redis&lt;/li&gt;
&lt;li&gt;在本地 &lt;code&gt;web_3&lt;/code&gt; 里把管理端验证码改成“后端生成 + Redis 存储 + 后端一次性校验”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后的结论也很明确：&lt;strong&gt;老后台页面上虽然有验证码，但登录流程里没有真正校验它；新版 &lt;code&gt;web_3&lt;/code&gt; 已经改成标准的 Redis 验证码闭环。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="我为什么要单独整理这篇笔记"&gt;&lt;a href="#%e6%88%91%e4%b8%ba%e4%bb%80%e4%b9%88%e8%a6%81%e5%8d%95%e7%8b%ac%e6%95%b4%e7%90%86%e8%bf%99%e7%af%87%e7%ac%94%e8%ae%b0" class="header-anchor"&gt;&lt;/a&gt;我为什么要单独整理这篇笔记
&lt;/h2&gt;&lt;p&gt;验证码这种东西很容易出现一种假安全状态：页面上明明有输入框，也有验证码图片，前端看起来一切都在，但真正决定安全性的不是页面上有没有控件，而是后端登录逻辑到底有没有做验证，以及验证结果有没有被一次性消费。&lt;/p&gt;
&lt;p&gt;这次排查让我更确定一件事：&lt;strong&gt;登录验证码必须按“生成、存储、提交、校验、销毁”这五步闭环来设计，缺任何一步都不算真正落地。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="老服务器后台验证码的真实逻辑"&gt;&lt;a href="#%e8%80%81%e6%9c%8d%e5%8a%a1%e5%99%a8%e5%90%8e%e5%8f%b0%e9%aa%8c%e8%af%81%e7%a0%81%e7%9a%84%e7%9c%9f%e5%ae%9e%e9%80%bb%e8%be%91" class="header-anchor"&gt;&lt;/a&gt;老服务器后台验证码的真实逻辑
&lt;/h2&gt;&lt;p&gt;这次我先通过 SSH 登录老服务器，只做了只读排查，没有改线上代码，也没有重启服务。&lt;/p&gt;
&lt;p&gt;我定位到的站点信息是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;域名 &lt;code&gt;yzdwx.yizhoudao.net&lt;/code&gt; 对应站点目录：&lt;code&gt;/www/wwwroot/yzdwx5.weitaibei.com/public_html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;应用入口：&lt;code&gt;/www/wwwroot/yzdwx5.weitaibei.com/public_html/index.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;实际业务代码目录：&lt;code&gt;/www/wwwroot/yzdwx5.weitaibei.com/system/application&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接着我去看后台登录页模板，确认页面上确实有验证码输入框和验证码图片。页面会请求：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/captcha
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;点击刷新时会请求：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/captcha?random=...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个路由最终来自 ThinkPHP 的验证码包。继续往下追，我确认到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;验证码图片确实是后端生成的&lt;/li&gt;
&lt;li&gt;验证码答案会写入 &lt;code&gt;Session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不是写在前端，也不是直接写入 Redis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更关键的是，我继续追到后台登录控制器之后发现，老后台登录方法虽然接收了 &lt;code&gt;$verify&lt;/code&gt; 参数，但实际逻辑里只做了这些事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户名判空&lt;/li&gt;
&lt;li&gt;密码判空&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;rbaclogin(...)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我没有看到任何真正的验证码校验逻辑，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;captcha_check($verify)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate(...captcha...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;手动对比验证码答案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以老服务器后台验证码的真实情况其实是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;页面上有验证码&lt;/li&gt;
&lt;li&gt;后端也会生成验证码图片&lt;/li&gt;
&lt;li&gt;验证码答案保存在 &lt;code&gt;session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;但登录业务流程里没有真正校验它&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着老版本后台的验证码大概率属于一种“界面还在，但校验已经失效或被漏掉”的状态。&lt;/p&gt;
&lt;h2 id="我是怎么确认这件事的"&gt;&lt;a href="#%e6%88%91%e6%98%af%e6%80%8e%e4%b9%88%e7%a1%ae%e8%ae%a4%e8%bf%99%e4%bb%b6%e4%ba%8b%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;我是怎么确认这件事的
&lt;/h2&gt;&lt;p&gt;这次排查不是靠猜，也不是只看前端页面，而是按一条比较稳的链路逐步确认的。&lt;/p&gt;
&lt;p&gt;我先确认 SSH 和站点目录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先通过本机 SSH 别名连到服务器&lt;/li&gt;
&lt;li&gt;查 Nginx 配置，确认 &lt;code&gt;yzdwx.yizhoudao.net&lt;/code&gt; 实际指向哪个目录&lt;/li&gt;
&lt;li&gt;查 PHP-FPM 和 Nginx 运行状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后我再往应用层追：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找后台登录模板&lt;/li&gt;
&lt;li&gt;找 &lt;code&gt;/captcha&lt;/code&gt; 路由来源&lt;/li&gt;
&lt;li&gt;找验证码生成逻辑&lt;/li&gt;
&lt;li&gt;找验证码校验逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后我再去看后台登录控制器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看 &lt;code&gt;login()&lt;/code&gt; 方法是否真正调用了验证码校验&lt;/li&gt;
&lt;li&gt;结果是没有&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个顺序的好处是不会被页面表现误导。只要登录控制器里没有执行验证码校验，那页面上验证码画得再完整，也只是“视觉存在”，不是“安全生效”。&lt;/p&gt;
&lt;h2 id="老-web-到底有没有用-redis"&gt;&lt;a href="#%e8%80%81-web-%e5%88%b0%e5%ba%95%e6%9c%89%e6%b2%a1%e6%9c%89%e7%94%a8-redis" class="header-anchor"&gt;&lt;/a&gt;老 Web 到底有没有用 Redis
&lt;/h2&gt;&lt;p&gt;这部分我也专门确认了一遍，因为很多项目都会出现一种情况：代码里带着 Redis 封装类，但线上根本没启用。&lt;/p&gt;
&lt;p&gt;这次我的结论是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码仓里确实有 Redis 工具类&lt;/li&gt;
&lt;li&gt;但线上这套老 Web 没查到业务代码实际调用它&lt;/li&gt;
&lt;li&gt;服务器本身也没发现 Redis 服务在运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我当时主要查了几类证据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ThinkPHP 的 session 配置&lt;/li&gt;
&lt;li&gt;PHP 的 &lt;code&gt;session.save_handler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/tmp&lt;/code&gt; 下是否存在大量 &lt;code&gt;sess_*&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;服务器上是否有 &lt;code&gt;redis-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;是否有 &lt;code&gt;redis-cli&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;是否监听了 &lt;code&gt;6379&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终确认到的事实是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Session 默认驱动不是 Redis&lt;/li&gt;
&lt;li&gt;PHP 当前会话处理器是 &lt;code&gt;files&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/tmp&lt;/code&gt; 下确实存在大量 &lt;code&gt;sess_*&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;服务器上没发现 Redis 进程&lt;/li&gt;
&lt;li&gt;没发现 &lt;code&gt;6379&lt;/code&gt; 监听&lt;/li&gt;
&lt;li&gt;也没发现 Redis 二进制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以更准确的说法不是“老项目完全没有 Redis”，而是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;老项目带过 Redis 封装代码，但当前运行中的老站没有实际启用 Redis，后台验证码也不在 Redis 里。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="老版本验证码的问题到底出在哪"&gt;&lt;a href="#%e8%80%81%e7%89%88%e6%9c%ac%e9%aa%8c%e8%af%81%e7%a0%81%e7%9a%84%e9%97%ae%e9%a2%98%e5%88%b0%e5%ba%95%e5%87%ba%e5%9c%a8%e5%93%aa" class="header-anchor"&gt;&lt;/a&gt;老版本验证码的问题到底出在哪
&lt;/h2&gt;&lt;p&gt;如果用一句话概括老后台的问题，我会说：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;它的问题不是“验证码生成失败”，而是“验证码没有进入真正的登录校验链路”。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;也就是说，旧系统表面上像是有验证码防护，但真正的登录安全流程里并没有用到它。这个问题比“没有验证码”更隐蔽，因为产品、测试甚至开发自己都可能先被页面骗过去。&lt;/p&gt;
&lt;p&gt;从规范角度看，老版本至少有两个明显缺口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;验证码答案存进了 Session，但登录接口没有校验&lt;/li&gt;
&lt;li&gt;验证码不是一次性消费，即使以后补校验，也容易留下复用问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我在本地-web_3-改成了什么逻辑"&gt;&lt;a href="#%e6%88%91%e5%9c%a8%e6%9c%ac%e5%9c%b0-web_3-%e6%94%b9%e6%88%90%e4%ba%86%e4%bb%80%e4%b9%88%e9%80%bb%e8%be%91" class="header-anchor"&gt;&lt;/a&gt;我在本地 web_3 改成了什么逻辑
&lt;/h2&gt;&lt;p&gt;确认完老系统问题之后，我在本地 &lt;code&gt;web_3&lt;/code&gt; 管理端把验证码流程改成了完整的后端闭环。&lt;/p&gt;
&lt;p&gt;现在的流程是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前端请求 &lt;code&gt;GET /api/admin/login/captcha&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;后端生成：
&lt;code&gt;captchaId&lt;/code&gt;
&lt;code&gt;4&lt;/code&gt; 位验证码
&lt;code&gt;Base64 PNG&lt;/code&gt; 图片&lt;/li&gt;
&lt;li&gt;后端把验证码答案写入 Redis&lt;/li&gt;
&lt;li&gt;前端只展示图片，同时保留 &lt;code&gt;captchaId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用户登录时，前端提交：
&lt;code&gt;username&lt;/code&gt;
&lt;code&gt;userpwd&lt;/code&gt;
&lt;code&gt;captchaId&lt;/code&gt;
&lt;code&gt;captchaCode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;后端先去 Redis 校验验证码&lt;/li&gt;
&lt;li&gt;校验时使用 &lt;code&gt;getAndDelete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;读取成功后立即删除&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个设计意味着一个验证码只有一次尝试机会。&lt;/p&gt;
&lt;p&gt;无论最终登录成功还是失败，这个验证码都会失效，不会继续留在系统里被重复提交。&lt;/p&gt;
&lt;h2 id="新版验证码链路的核心规范"&gt;&lt;a href="#%e6%96%b0%e7%89%88%e9%aa%8c%e8%af%81%e7%a0%81%e9%93%be%e8%b7%af%e7%9a%84%e6%a0%b8%e5%bf%83%e8%a7%84%e8%8c%83" class="header-anchor"&gt;&lt;/a&gt;新版验证码链路的核心规范
&lt;/h2&gt;&lt;p&gt;结合这次改造，我把 Web 登录验证码应该遵守的规范总结成下面这几条。&lt;/p&gt;
&lt;h3 id="规范-1验证码必须由后端生成"&gt;&lt;a href="#%e8%a7%84%e8%8c%83-1%e9%aa%8c%e8%af%81%e7%a0%81%e5%bf%85%e9%a1%bb%e7%94%b1%e5%90%8e%e7%ab%af%e7%94%9f%e6%88%90" class="header-anchor"&gt;&lt;/a&gt;规范 1：验证码必须由后端生成
&lt;/h3&gt;&lt;p&gt;验证码图片、验证码答案和 &lt;code&gt;captchaId&lt;/code&gt; 都必须由后端统一生成。前端只负责展示，不参与答案生成，也不保存正确答案。&lt;/p&gt;
&lt;p&gt;这样可以避免前端暴露正确结果，保证校验基准只存在于服务端。&lt;/p&gt;
&lt;h3 id="规范-2验证码答案必须保存在短期存储中"&gt;&lt;a href="#%e8%a7%84%e8%8c%83-2%e9%aa%8c%e8%af%81%e7%a0%81%e7%ad%94%e6%a1%88%e5%bf%85%e9%a1%bb%e4%bf%9d%e5%ad%98%e5%9c%a8%e7%9f%ad%e6%9c%9f%e5%ad%98%e5%82%a8%e4%b8%ad" class="header-anchor"&gt;&lt;/a&gt;规范 2：验证码答案必须保存在短期存储中
&lt;/h3&gt;&lt;p&gt;这次我选的是 Redis。它很适合这种短时、一次性、低价值但安全敏感的数据。&lt;/p&gt;
&lt;p&gt;相比 &lt;code&gt;session/file&lt;/code&gt;，Redis 更适合后续扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以设置 TTL&lt;/li&gt;
&lt;li&gt;可以做原子删除&lt;/li&gt;
&lt;li&gt;可以支持多机共享&lt;/li&gt;
&lt;li&gt;可以承接登录风控类临时状态&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="规范-3登录接口必须在鉴权前先校验验证码"&gt;&lt;a href="#%e8%a7%84%e8%8c%83-3%e7%99%bb%e5%bd%95%e6%8e%a5%e5%8f%a3%e5%bf%85%e9%a1%bb%e5%9c%a8%e9%89%b4%e6%9d%83%e5%89%8d%e5%85%88%e6%a0%a1%e9%aa%8c%e9%aa%8c%e8%af%81%e7%a0%81" class="header-anchor"&gt;&lt;/a&gt;规范 3：登录接口必须在鉴权前先校验验证码
&lt;/h3&gt;&lt;p&gt;验证码校验不能放在一个“看起来有，但不影响主流程”的边缘位置。它必须在用户名密码真正进入登录鉴权前完成。&lt;/p&gt;
&lt;p&gt;也就是说，顺序应该是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;先校验验证码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;再校验账号密码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;最后建立登录态
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果验证码失败，后面的账号密码逻辑就不应该继续执行。&lt;/p&gt;
&lt;h3 id="规范-4验证码必须一次性消费"&gt;&lt;a href="#%e8%a7%84%e8%8c%83-4%e9%aa%8c%e8%af%81%e7%a0%81%e5%bf%85%e9%a1%bb%e4%b8%80%e6%ac%a1%e6%80%a7%e6%b6%88%e8%b4%b9" class="header-anchor"&gt;&lt;/a&gt;规范 4：验证码必须一次性消费
&lt;/h3&gt;&lt;p&gt;这次我专门用了 &lt;code&gt;getAndDelete&lt;/code&gt; 这种读取即删除的方式，目的是确保验证码永远只能用一次。&lt;/p&gt;
&lt;p&gt;这个约束非常重要，因为如果验证码校验通过后不立刻销毁，就可能出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一验证码被重复利用&lt;/li&gt;
&lt;li&gt;自动化脚本反复撞同一个验证码&lt;/li&gt;
&lt;li&gt;失败重试时复用旧验证码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一次性消费是验证码机制真正生效的关键之一。&lt;/p&gt;
&lt;h3 id="规范-5成功和失败都要失效"&gt;&lt;a href="#%e8%a7%84%e8%8c%83-5%e6%88%90%e5%8a%9f%e5%92%8c%e5%a4%b1%e8%b4%a5%e9%83%bd%e8%a6%81%e5%a4%b1%e6%95%88" class="header-anchor"&gt;&lt;/a&gt;规范 5：成功和失败都要失效
&lt;/h3&gt;&lt;p&gt;很多人只会在“登录成功”时删验证码，但这其实还不够。&lt;/p&gt;
&lt;p&gt;更稳的做法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;校验成功后立即删除&lt;/li&gt;
&lt;li&gt;校验失败也要删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为验证码本来就是一次挑战，不应该允许用户拿着同一个 &lt;code&gt;captchaId&lt;/code&gt; 连续试很多次。&lt;/p&gt;
&lt;h2 id="这次改成-redis-后我认为最实际的好处"&gt;&lt;a href="#%e8%bf%99%e6%ac%a1%e6%94%b9%e6%88%90-redis-%e5%90%8e%e6%88%91%e8%ae%a4%e4%b8%ba%e6%9c%80%e5%ae%9e%e9%99%85%e7%9a%84%e5%a5%bd%e5%a4%84" class="header-anchor"&gt;&lt;/a&gt;这次改成 Redis 后，我认为最实际的好处
&lt;/h2&gt;&lt;p&gt;这次改造给我的直接感受，不是“技术更高级了”，而是整个登录安全链路终于闭合了。&lt;/p&gt;
&lt;p&gt;主要好处有这些：&lt;/p&gt;
&lt;h3 id="逻辑闭环完整"&gt;&lt;a href="#%e9%80%bb%e8%be%91%e9%97%ad%e7%8e%af%e5%ae%8c%e6%95%b4" class="header-anchor"&gt;&lt;/a&gt;逻辑闭环完整
&lt;/h3&gt;&lt;p&gt;老版本是“有验证码页面，但登录不校验”。&lt;/p&gt;
&lt;p&gt;现在变成了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后端生成&lt;/li&gt;
&lt;li&gt;后端存储&lt;/li&gt;
&lt;li&gt;前端展示&lt;/li&gt;
&lt;li&gt;后端校验&lt;/li&gt;
&lt;li&gt;一次性消费&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这才是一条完整的安全链路。&lt;/p&gt;
&lt;h3 id="不再依赖本地-session-文件"&gt;&lt;a href="#%e4%b8%8d%e5%86%8d%e4%be%9d%e8%b5%96%e6%9c%ac%e5%9c%b0-session-%e6%96%87%e4%bb%b6" class="header-anchor"&gt;&lt;/a&gt;不再依赖本地 session 文件
&lt;/h3&gt;&lt;p&gt;老站的验证码答案实际落在 &lt;code&gt;session/file&lt;/code&gt; 和 &lt;code&gt;/tmp sess_*&lt;/code&gt; 上。&lt;/p&gt;
&lt;p&gt;这套方案虽然在单机环境里能跑，但它天然更偏运行时实现，不够适合后续扩展。Redis 对这类短期临时数据更合适，也更容易观察和控制。&lt;/p&gt;
&lt;h3 id="一次性验证码更自然"&gt;&lt;a href="#%e4%b8%80%e6%ac%a1%e6%80%a7%e9%aa%8c%e8%af%81%e7%a0%81%e6%9b%b4%e8%87%aa%e7%84%b6" class="header-anchor"&gt;&lt;/a&gt;一次性验证码更自然
&lt;/h3&gt;&lt;p&gt;Redis 很适合实现这种需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TTL 过期&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getAndDelete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;成功失败都删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些语义在验证码场景里非常自然，不需要绕很多额外逻辑。&lt;/p&gt;
&lt;h3 id="多机部署更友好"&gt;&lt;a href="#%e5%a4%9a%e6%9c%ba%e9%83%a8%e7%bd%b2%e6%9b%b4%e5%8f%8b%e5%a5%bd" class="header-anchor"&gt;&lt;/a&gt;多机部署更友好
&lt;/h3&gt;&lt;p&gt;如果以后不是单机，而是多台 Web 共同处理请求，那么 &lt;code&gt;session/file&lt;/code&gt; 很容易出现跨机器不一致问题。&lt;/p&gt;
&lt;p&gt;Redis 作为共享存储，可以让所有 Web 节点读到同一份验证码状态，这对后续扩容非常关键。&lt;/p&gt;
&lt;h3 id="后续风控能力更容易接上"&gt;&lt;a href="#%e5%90%8e%e7%bb%ad%e9%a3%8e%e6%8e%a7%e8%83%bd%e5%8a%9b%e6%9b%b4%e5%ae%b9%e6%98%93%e6%8e%a5%e4%b8%8a" class="header-anchor"&gt;&lt;/a&gt;后续风控能力更容易接上
&lt;/h3&gt;&lt;p&gt;验证码逻辑进入 Redis 之后，很多安全相关能力也更容易继续往上加，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;短信验证码&lt;/li&gt;
&lt;li&gt;登录失败次数限制&lt;/li&gt;
&lt;li&gt;风控计数&lt;/li&gt;
&lt;li&gt;临时 token&lt;/li&gt;
&lt;li&gt;AI/RAG 相关短期缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从工程上看，这相当于把“登录临时态”统一收进了一个更适合治理的位置。&lt;/p&gt;
&lt;h2 id="这次我顺手做的配套工作"&gt;&lt;a href="#%e8%bf%99%e6%ac%a1%e6%88%91%e9%a1%ba%e6%89%8b%e5%81%9a%e7%9a%84%e9%85%8d%e5%a5%97%e5%b7%a5%e4%bd%9c" class="header-anchor"&gt;&lt;/a&gt;这次我顺手做的配套工作
&lt;/h2&gt;&lt;p&gt;为了让新版 &lt;code&gt;web_3&lt;/code&gt; 本地链路真正跑通，这次我还顺手处理了几项配套问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地临时启动了一个 Redis，监听 &lt;code&gt;127.0.0.1:6379&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修了 &lt;code&gt;backend-java&lt;/code&gt; 启动时的 YAML 配置问题&lt;/li&gt;
&lt;li&gt;清理了后端端口占用&lt;/li&gt;
&lt;li&gt;把管理端前端改成支持局域网访问，监听 &lt;code&gt;0.0.0.0:9511&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调整了登录页移动端和窄屏适配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些改动虽然不是验证码主逻辑，但它们决定了整个方案能不能从“代码写好了”真正走到“本地完整跑通”。&lt;/p&gt;
&lt;h2 id="我最后得出的规范结论"&gt;&lt;a href="#%e6%88%91%e6%9c%80%e5%90%8e%e5%be%97%e5%87%ba%e7%9a%84%e8%a7%84%e8%8c%83%e7%bb%93%e8%ae%ba" class="header-anchor"&gt;&lt;/a&gt;我最后得出的规范结论
&lt;/h2&gt;&lt;p&gt;这次排查和改造之后，我对 Web 登录验证码的规范要求基本固定成下面这套：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;验证码必须由后端生成&lt;/li&gt;
&lt;li&gt;验证码答案必须只保存在后端&lt;/li&gt;
&lt;li&gt;前端只展示图片和提交 &lt;code&gt;captchaId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;登录接口必须先校验验证码，再校验账号密码&lt;/li&gt;
&lt;li&gt;验证码必须一次性消费&lt;/li&gt;
&lt;li&gt;成功失败都必须失效&lt;/li&gt;
&lt;li&gt;短期验证码优先放 Redis，而不是依赖本地 session 文件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果少了其中任何一条，我都不会认为这套验证码是真正“规范落地”的。&lt;/p&gt;
&lt;h2 id="小结"&gt;&lt;a href="#%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;小结
&lt;/h2&gt;&lt;p&gt;一句话总结这次工作：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;老服务器后台验证码是 ThinkPHP 后端生成、保存到 Session，但后台登录代码里没有真正校验它；老服务器当前也没有实际运行 Redis。新版 &lt;code&gt;web_3&lt;/code&gt; 则已经改成标准的 Redis 验证码流程：后端生成、Redis 保存、前端展示、后端校验、一次性消费、成功失败都删除。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;对我来说，这次最重要的收获不是“把验证码改成 Redis”这么简单，而是把验证码这件事从“页面上看起来有”推进到了“后端链路真正闭环”。只有做到这一点，验证码才不是装饰，而是登录安全的一部分。&lt;/p&gt;</description></item></channel></rss>