优先选dchest/captcha:结构清晰、可替换存储、无全局状态污染,与iris天然契合;base64captcha易因rand.seed冲突导致并发卡死、图片空白或校验失败。

用 dchest/captcha 还是 base64Captcha?选错驱动会卡死并发
直接结论:Iris 项目里优先用 dchest/captcha,别碰 base64Captcha。后者默认内存存储、不支持自定义 store、并发生成时容易触发 rand.Seed 冲突,实测在压测下会出现验证码图片空白或校验始终失败。
dchest/captcha 虽然 API 略旧,但结构清晰、可替换存储、无全局状态污染,和 Iris 的 MVC 生命周期天然契合。关键点在于它把 captcha ID 和答案分离管理——ID 由前端传入路径(如 /captcha/abc123),答案则存在你指定的 store 里,不依赖 session 或内存。
- 必须调用
captcha.SetCustomStore替换默认内存 store,否则服务重启后所有未过期验证码失效 - 不要在每次请求中 new
captcha.DriverDigit,复用一个实例即可;重复初始化会加载字体文件多次,吃内存 - 验证码长度设为 4–5 位,
dchest/captcha默认字符集含 0/O/1/I,建议手动过滤掉易混淆字符
验证码图片路由怎么写?别用中间件,GET handler 才可靠
Iris 中最常踩的坑,就是把验证码生成塞进中间件。结果要么图片不刷新,要么 captcha_id 传不到前端,根本原因是中间件执行时机早于 controller context 绑定,Ctx.Params() 拿不到路径参数。
正确做法是单独注册一个 GET 路由,对应 controller 方法:
func (c *CaptchaController) CaptchaImage() {
captchaID := c.Ctx.Params().Get("captcha_id")
if captchaID == "" {
captchaID = captcha.New()
}
c.responseCaptchaImage(captchaID, 200, 50)
}
这个方法只做一件事:按需生成或重载图片,并返回 PNG 流。注意两点:
- 响应头必须加
Cache-Control: no-cache,否则浏览器缓存导致用户点“换一张”没反应 - 路径参数
captcha_id不要硬编码,让前端控制生成时机,比如登录页 mounted 时发一次GET /captcha/,后端自动生成新 ID 并重定向到带 ID 的路径
校验逻辑为什么总失败?绑定上下文比校验本身更重要
校验失败的主因从来不是 captcha.VerifyString 写错了,而是前后端上下文没对齐。常见错误模式:
- 前端把
captcha_id存 localStorage,后端只查 Redis —— 一旦用户多开标签页,ID 被覆盖,校验必挂 - 登录接口直接接收
captcha_id+ 用户输入值,不做任何来源校验 —— 攻击者遍历 ID(如 1~1000)批量爆破 - 校验成功后没调
captcha.Delete(id),导致同一验证码被重复提交
安全做法是绑定请求指纹:
有 session:生成时存 session.Set("captcha_id", id),登录时先比对 session 中的 ID 是否一致;
无 session(如 API 登录):用 md5(ip + user-agent + salt) 拼出 key 前缀,再拼上 captcha_id 存 Redis,校验前先检查 key 是否匹配。
Redis 存储怎么配?过期时间不能只信默认值
dchest/captcha 默认过期是 10 分钟,但若依等成熟框架都设为 2 分钟。这不是保守,而是权衡:太短,用户输得慢就失效;太长,重放攻击窗口变大。
你得自己接管过期逻辑:
- 调用
captcha.SetCustomStore时,传入的 store 必须实现Set(key, value, exp int),其中exp显式传 120(秒) - 别依赖
captcha.Generate返回的过期时间字段,它只是个提示,实际以 store 设置为准 - Redis key 命名建议加前缀,比如
captcha:login:+ id,避免和其他业务 key 冲突
最后提醒一句:验证码不是防所有攻击的银弹。它只防自动化脚本,不防人工打码或 OCR。真正要加固登录,得配合 IP 限流、密码错误锁定、JWT 短期有效这些组合拳——而验证码,只是第一道门把手,拧紧了才值得后面继续装锁。











