应返回含完整 data url 的 base64 字符串,image 字段为 string 类型且 json tag 为 "image";cutx 需单独整型字段;禁用缓存须设 no-store;redis 验证需绑定时间戳并即时删除 key。

直接返回 base64 图片字符串,别传二进制流
Gin 处理验证码时,c.Writer.Write() 写 PNG 二进制流看似直观,但前端必须额外用 Blob + URL.createObjectURL 才能渲染,还容易因 MIME 类型、缓存、跨域出问题。更稳妥的做法是:后端生成 base64 字符串,前端直接塞进 <img src="data:image/png;base64,...">。省去接口、无需额外请求,也规避了浏览器对图片响应的默认缓存策略干扰。
用 base64Captcha 生成并返回结构体
不要自己拼接 data URL 前缀,base64Captcha.Generate() 返回的 b64s 已含完整 data:image/png;base64,... 字符串。关键点在结构体定义和字段命名:
-
Image字段必须是string类型,且 JSON tag 设为"image" - 若需拼图类验证码,
CutX必须单独作为整型字段返回(如CutX int `json:"cut_x"`),绝不能藏进图片或 base64 字符串里——OCR 可轻易提取 - 避免返回冗余字段(如原始 answer、id 的 raw bytes),防止调试信息泄露
禁用浏览器缓存必须显式设置 Header
即使你每次生成新图片,浏览器仍可能复用旧响应。仅靠 Cache-Control: no-cache 不够,必须用:
c.Header("Cache-Control", "no-store, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
注意:no-store 是硬性要求,no-cache 仍允许浏览器发条件请求(If-None-Match)并缓存响应体;而 no-store 禁止任何存储行为。实测 Chrome/Firefox 对 no-store 响应不会写入磁盘缓存。
Redis 存储验证态时必须绑定时间戳防重放
单纯用 store.Verify(id, code, true)(来自 base64Captcha 内存 store)有严重风险:ID 可被重复提交、答案可暴力穷举、过期逻辑依赖内存 GC 不可控。正确做法是:
- 生成时:把
id + timestamp拼成 Redis key(如"captcha:session_abc123:1725502320"),value 存{answer: "aB3x9", cut_x: 82},TTL 设为120 - 验证时:先查 key 是否存在,再检查当前时间与 key 中 timestamp 差值是否 ≤120 秒,两者缺一不可
- 验证通过后立即
DEL该 key,防止二次使用
这一步最容易被跳过——很多人只校验答案,却忘了攻击者可以截获一次响应后反复重放,只要 ID 未失效就始终有效。











