验证码“随机变化”靠每次请求生成新id、绑定上下文并服务端主动失效,否则必然被绕过或复用;前端图不变主因是复用captcha实例、未清旧id或get缓存,须每次调用generate()、加no-cache头、带时间戳参数,并确保id随新图下发。

直接说结论:验证码“随机变化”不是靠前端轮询或后端无脑重生成,而是靠每次请求生成新 ID + 绑定上下文 + 服务端主动失效——否则必然被绕过或复用。
为什么每次调用 captcha.Generate() 后前端看到的图还是旧的?
常见现象是前端点击“换一张”按钮,但图片 base64 字符串没变,或反复请求返回同一张图。根本原因不是库有问题,而是你复用了 captcha 实例且没清空旧 ID,或者前端缓存了响应(尤其是 GET 请求)。
- 确保每次生成都调用全新的
captcha.Generate(),不要在 handler 外全局复用单个captcha对象 - 后端返回时加 HTTP 头:
c.Header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") - 前端发起请求时带时间戳参数(如
/captcha?t=1723513281)或用 POST 避免 GET 缓存 - 检查是否误把
id存在 cookie/session 里并重复传给前端——ID 必须随新图一起下发,不能复用旧 ID
base64Captcha.NewDriverDigit 和 base64Captcha.DriverString 的随机性差异
两者都依赖 rand,但默认种子只在包初始化时设一次。并发高时若未显式隔离,可能短暂出现相似图。这不是 bug,是设计使然。
-
NewDriverDigit:纯数字,抗 OCR 弱,适合登录页简单校验;参数中skew(倾斜度)、dotCount(噪点数)直接影响视觉随机感,建议设为0.7和80起步 -
DriverString:支持大小写字母+数字,Source字段决定字符池,务必避免只填"01"这类极简集;Fonts数组至少含一个可用字体文件路径,否则 fallback 到系统默认,渲染一致性差 - 无论哪种 driver,都必须调用
.ConvertFonts()(DriverString)或直接传参(NewDriverDigit),否则字体加载失败,图会空白或报错
如何让“换一张”真正生成新图且不可预测?
关键不在前端怎么点,而在后端是否切断旧 ID 生命周期、并拒绝旧 ID 重放。
- 生成新图时,先调用
store.Delete(oldID)(如果知道 oldID),再调用captcha.Generate()得到新 ID —— 不要等用户点“换一张”才删,而应在生成新图前主动清理关联上下文 - 若使用 session 绑定,生成新图时同步更新
session.Set("captcha_id", newID);校验时先比对 session 中的 ID 是否与请求 ID 一致,不一致直接拒 - 若无 session(如开放 API),用
hash(Real-IP + User-Agent + salt)作存储 key 前缀,避免攻击者固定 IP 批量刷 ID -
store.Verify(id, input, true)的第三个参数必须为true,否则验证成功后 ID 仍有效,可被重放
分布式部署下验证码 ID 冲突或失效怎么办?
默认 base64Captcha.DefaultMemStore 是纯内存,重启即丢、多实例不同步。看似“随机变化”,实则各节点 ID 空间重叠,校验永远失败。
- 必须替换为共享存储:Redis 是最简方案,实现
base64Captcha.Store接口即可,注意Set()要设 TTL(建议time.Minute * 5),Get()的clear参数要真实触发DEL - 不要自己拼 Redis key,直接用库推荐格式:
"captcha:" + id,避免 key 冲突 - 测试时关掉 debug 模式,否则日志里会明文打印验证码答案,线上等于裸奔
最易被忽略的一点:验证码的“随机”本质是服务端可控的不可预测性,不是视觉花哨程度。哪怕图看起来一样,只要每次 ID 唯一、绑定上下文、即时失效,就满足安全要求;反之,图再扭曲,ID 可遍历、可重放、跨实例失效,再“随机”也没用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











