Web 登录验证码规范:从老后台排查到 Redis 一次性校验
这篇笔记记录我最近围绕 Web 管理端登录验证码做的一次完整排查和改造。事情的起点很简单:我想确认老服务器上的后台验证码到底是不是“真的在校验”,以及新版 web_3 应该把验证码流程收敛成什么样,才算一套真正完整的登录安全链路。
这次我主要做了三件事:
- 排查老服务器
yzdwx.yizhoudao.net/admin/的验证码真实逻辑 - 确认老 Web 是否真的启用了 Redis
- 在本地
web_3里把管理端验证码改成“后端生成 + Redis 存储 + 后端一次性校验”
最后的结论也很明确:老后台页面上虽然有验证码,但登录流程里没有真正校验它;新版 web_3 已经改成标准的 Redis 验证码闭环。
我为什么要单独整理这篇笔记
验证码这种东西很容易出现一种假安全状态:页面上明明有输入框,也有验证码图片,前端看起来一切都在,但真正决定安全性的不是页面上有没有控件,而是后端登录逻辑到底有没有做验证,以及验证结果有没有被一次性消费。
这次排查让我更确定一件事:登录验证码必须按“生成、存储、提交、校验、销毁”这五步闭环来设计,缺任何一步都不算真正落地。
老服务器后台验证码的真实逻辑
这次我先通过 SSH 登录老服务器,只做了只读排查,没有改线上代码,也没有重启服务。
我定位到的站点信息是:
- 域名
yzdwx.yizhoudao.net对应站点目录:/www/wwwroot/yzdwx5.weitaibei.com/public_html - 应用入口:
/www/wwwroot/yzdwx5.weitaibei.com/public_html/index.php - 实际业务代码目录:
/www/wwwroot/yzdwx5.weitaibei.com/system/application
接着我去看后台登录页模板,确认页面上确实有验证码输入框和验证码图片。页面会请求:
/captcha
点击刷新时会请求:
/captcha?random=...
这个路由最终来自 ThinkPHP 的验证码包。继续往下追,我确认到:
- 验证码图片确实是后端生成的
- 验证码答案会写入
Session - 不是写在前端,也不是直接写入 Redis
更关键的是,我继续追到后台登录控制器之后发现,老后台登录方法虽然接收了 $verify 参数,但实际逻辑里只做了这些事:
- 用户名判空
- 密码判空
- 调用
rbaclogin(...)
我没有看到任何真正的验证码校验逻辑,比如:
captcha_check($verify)validate(...captcha...)- 手动对比验证码答案
所以老服务器后台验证码的真实情况其实是:
- 页面上有验证码
- 后端也会生成验证码图片
- 验证码答案保存在
session - 但登录业务流程里没有真正校验它
这意味着老版本后台的验证码大概率属于一种“界面还在,但校验已经失效或被漏掉”的状态。
我是怎么确认这件事的
这次排查不是靠猜,也不是只看前端页面,而是按一条比较稳的链路逐步确认的。
我先确认 SSH 和站点目录:
- 先通过本机 SSH 别名连到服务器
- 查 Nginx 配置,确认
yzdwx.yizhoudao.net实际指向哪个目录 - 查 PHP-FPM 和 Nginx 运行状态
然后我再往应用层追:
- 找后台登录模板
- 找
/captcha路由来源 - 找验证码生成逻辑
- 找验证码校验逻辑
最后我再去看后台登录控制器:
- 看
login()方法是否真正调用了验证码校验 - 结果是没有
这个顺序的好处是不会被页面表现误导。只要登录控制器里没有执行验证码校验,那页面上验证码画得再完整,也只是“视觉存在”,不是“安全生效”。
老 Web 到底有没有用 Redis
这部分我也专门确认了一遍,因为很多项目都会出现一种情况:代码里带着 Redis 封装类,但线上根本没启用。
这次我的结论是:
- 代码仓里确实有 Redis 工具类
- 但线上这套老 Web 没查到业务代码实际调用它
- 服务器本身也没发现 Redis 服务在运行
我当时主要查了几类证据:
- ThinkPHP 的 session 配置
- PHP 的
session.save_handler /tmp下是否存在大量sess_*文件- 服务器上是否有
redis-server - 是否有
redis-cli - 是否监听了
6379
最终确认到的事实是:
- Session 默认驱动不是 Redis
- PHP 当前会话处理器是
files /tmp下确实存在大量sess_*文件- 服务器上没发现 Redis 进程
- 没发现
6379监听 - 也没发现 Redis 二进制
所以更准确的说法不是“老项目完全没有 Redis”,而是:
老项目带过 Redis 封装代码,但当前运行中的老站没有实际启用 Redis,后台验证码也不在 Redis 里。
老版本验证码的问题到底出在哪
如果用一句话概括老后台的问题,我会说:
它的问题不是“验证码生成失败”,而是“验证码没有进入真正的登录校验链路”。
也就是说,旧系统表面上像是有验证码防护,但真正的登录安全流程里并没有用到它。这个问题比“没有验证码”更隐蔽,因为产品、测试甚至开发自己都可能先被页面骗过去。
从规范角度看,老版本至少有两个明显缺口:
- 验证码答案存进了 Session,但登录接口没有校验
- 验证码不是一次性消费,即使以后补校验,也容易留下复用问题
我在本地 web_3 改成了什么逻辑
确认完老系统问题之后,我在本地 web_3 管理端把验证码流程改成了完整的后端闭环。
现在的流程是:
- 前端请求
GET /api/admin/login/captcha - 后端生成:
captchaId4位验证码Base64 PNG图片 - 后端把验证码答案写入 Redis
- 前端只展示图片,同时保留
captchaId - 用户登录时,前端提交:
usernameuserpwdcaptchaIdcaptchaCode - 后端先去 Redis 校验验证码
- 校验时使用
getAndDelete - 读取成功后立即删除
这个设计意味着一个验证码只有一次尝试机会。
无论最终登录成功还是失败,这个验证码都会失效,不会继续留在系统里被重复提交。
新版验证码链路的核心规范
结合这次改造,我把 Web 登录验证码应该遵守的规范总结成下面这几条。
规范 1:验证码必须由后端生成
验证码图片、验证码答案和 captchaId 都必须由后端统一生成。前端只负责展示,不参与答案生成,也不保存正确答案。
这样可以避免前端暴露正确结果,保证校验基准只存在于服务端。
规范 2:验证码答案必须保存在短期存储中
这次我选的是 Redis。它很适合这种短时、一次性、低价值但安全敏感的数据。
相比 session/file,Redis 更适合后续扩展:
- 可以设置 TTL
- 可以做原子删除
- 可以支持多机共享
- 可以承接登录风控类临时状态
规范 3:登录接口必须在鉴权前先校验验证码
验证码校验不能放在一个“看起来有,但不影响主流程”的边缘位置。它必须在用户名密码真正进入登录鉴权前完成。
也就是说,顺序应该是:
先校验验证码
再校验账号密码
最后建立登录态
如果验证码失败,后面的账号密码逻辑就不应该继续执行。
规范 4:验证码必须一次性消费
这次我专门用了 getAndDelete 这种读取即删除的方式,目的是确保验证码永远只能用一次。
这个约束非常重要,因为如果验证码校验通过后不立刻销毁,就可能出现:
- 同一验证码被重复利用
- 自动化脚本反复撞同一个验证码
- 失败重试时复用旧验证码
一次性消费是验证码机制真正生效的关键之一。
规范 5:成功和失败都要失效
很多人只会在“登录成功”时删验证码,但这其实还不够。
更稳的做法是:
- 校验成功后立即删除
- 校验失败也要删除
因为验证码本来就是一次挑战,不应该允许用户拿着同一个 captchaId 连续试很多次。
这次改成 Redis 后,我认为最实际的好处
这次改造给我的直接感受,不是“技术更高级了”,而是整个登录安全链路终于闭合了。
主要好处有这些:
逻辑闭环完整
老版本是“有验证码页面,但登录不校验”。
现在变成了:
- 后端生成
- 后端存储
- 前端展示
- 后端校验
- 一次性消费
这才是一条完整的安全链路。
不再依赖本地 session 文件
老站的验证码答案实际落在 session/file 和 /tmp sess_* 上。
这套方案虽然在单机环境里能跑,但它天然更偏运行时实现,不够适合后续扩展。Redis 对这类短期临时数据更合适,也更容易观察和控制。
一次性验证码更自然
Redis 很适合实现这种需求:
- TTL 过期
getAndDelete- 成功失败都删除
这些语义在验证码场景里非常自然,不需要绕很多额外逻辑。
多机部署更友好
如果以后不是单机,而是多台 Web 共同处理请求,那么 session/file 很容易出现跨机器不一致问题。
Redis 作为共享存储,可以让所有 Web 节点读到同一份验证码状态,这对后续扩容非常关键。
后续风控能力更容易接上
验证码逻辑进入 Redis 之后,很多安全相关能力也更容易继续往上加,比如:
- 短信验证码
- 登录失败次数限制
- 风控计数
- 临时 token
- AI/RAG 相关短期缓存
从工程上看,这相当于把“登录临时态”统一收进了一个更适合治理的位置。
这次我顺手做的配套工作
为了让新版 web_3 本地链路真正跑通,这次我还顺手处理了几项配套问题:
- 本地临时启动了一个 Redis,监听
127.0.0.1:6379 - 修了
backend-java启动时的 YAML 配置问题 - 清理了后端端口占用
- 把管理端前端改成支持局域网访问,监听
0.0.0.0:9511 - 调整了登录页移动端和窄屏适配
这些改动虽然不是验证码主逻辑,但它们决定了整个方案能不能从“代码写好了”真正走到“本地完整跑通”。
我最后得出的规范结论
这次排查和改造之后,我对 Web 登录验证码的规范要求基本固定成下面这套:
- 验证码必须由后端生成
- 验证码答案必须只保存在后端
- 前端只展示图片和提交
captchaId - 登录接口必须先校验验证码,再校验账号密码
- 验证码必须一次性消费
- 成功失败都必须失效
- 短期验证码优先放 Redis,而不是依赖本地 session 文件
如果少了其中任何一条,我都不会认为这套验证码是真正“规范落地”的。
小结
一句话总结这次工作:
老服务器后台验证码是 ThinkPHP 后端生成、保存到 Session,但后台登录代码里没有真正校验它;老服务器当前也没有实际运行 Redis。新版
web_3则已经改成标准的 Redis 验证码流程:后端生成、Redis 保存、前端展示、后端校验、一次性消费、成功失败都删除。
对我来说,这次最重要的收获不是“把验证码改成 Redis”这么简单,而是把验证码这件事从“页面上看起来有”推进到了“后端链路真正闭环”。只有做到这一点,验证码才不是装饰,而是登录安全的一部分。