必须每次生成新验证码时返回全新ID并绑定会话,前端严格使用该ID提交,后端校验后立即销毁,同时复用driver实例、增强干扰策略以防止OCR识别和暴力遍历攻击。

直接用 base64Captcha 提供的 Generate() 就能动态生成新验证码,但「动态切换」不是调一次接口就行——关键在前后端协同刷新、ID 生命周期管理、以及避免被批量重放。
如何让前端点击「换一张」时后端真生成新验证码
常见错误是前端只改了图片 base64 但没传新 id,或后端复用旧 id 导致校验错乱。必须每次生成都返回全新 id,且前端严格绑定该 id 到本次表单提交。
- 后端路由(如
GET /captcha)每次请求都调用captcha.Generate(),绝不缓存或复用上一次的id - 前端收到响应后,把新
id存入隐藏域或 React state,后续登录/注册请求必须携带它 - 禁止前端自行拼接
id(比如加个时间戳后缀),id只能由后端生成并完全信任 - 如果用 axios,换图按钮点击后应清空输入框,并重置
input.value = '',避免用户输旧码却配新图
为什么不能复用同一个 base64Captcha.Captcha 实例
base64Captcha.NewCaptcha(driver, store) 创建的是无状态工厂,但驱动(driver)本身含字体加载、绘图上下文等开销。反复 new 它会导致:
- 字体文件重复读取(尤其
.ttc文件较大时) - 并发下
rand.Seed()冲突(老版本库中常见) - GC 压力上升,实测 QPS 超 500 后内存泄漏明显
正确做法:全局复用一个 driver 实例(如 var digitDriver = base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)),再用它创建唯一 captcha 实例:var captcha = base64Captcha.NewCaptcha(digitDriver, store)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
校验时如何防止 id 被遍历攻击
单纯暴露 /verify?id=xxx&code=1234 是高危设计。攻击者可写脚本从 1 遍历到 10000,暴力尝试所有 id 对应的验证码。
- 必须将
id绑定到具体会话:登录页场景下,生成后立即存入session.Set("captcha_id", id),校验前先比对session.Get("captcha_id") == id - 无 session 场景(如开放 API),用
sha256(ip + user-agent + salt)作存储 key 前缀,salt每次生成时随机生成并存入 store -
store.Verify(id, input, true)第三个参数设为true,确保校验成功后立刻删除,杜绝重放 - 所有生成接口不走鉴权中间件(如 JWT check),否则用户未登录就拿不到验证码
字体与干扰策略直接影响 OCR 破解难度
默认的 DriverDigit 抗 OCR 能力弱,纯数字+直线干扰线极易被 tesseract 识别。实际部署必须调整:
- 优先用
DriverString,字符集包含大小写字母+数字,长度设为 5–6(Length: 5) - 开启复合干扰:
ShowLineOptions: base64Captcha.OptionShowSineLine | base64Captcha.OptionShowSlimeLine - 背景色别用纯白:
BgColor: &color.RGBA{R: 240, G: 240, B: 240, A: 255},加点灰度降低对比度 - 字体务必指定真实存在的中文字体文件(如
wqy-microhei.ttc),Windows 下路径要绝对或加embed.FS打包,否则运行时报open xxx.ttc: no such file
真正难搞的不是生成逻辑,而是让每个 id 的生命周期干净可控——生成、绑定、校验、销毁,四步缺一不可,漏掉任意一环,验证码就形同虚设。

















