必须自己实现base64Captcha.Store接口,DefaultMemStore仅适用于单机调试;Redis存储需完整实现Set、Get、Verify三个方法,Set须带TTL,Get需处理redis.Nil,Verify应复用Get逻辑并严格传递clear参数,key命名需加命名空间前缀,context须每次显式传入而非全局复用。

base64Captcha.Store 接口必须自己实现
直接用 base64Captcha.DefaultMemStore 只能跑单机,多实例或重启后验证码就丢——这不是 bug,是设计使然。Redis 存储必须手动实现 base64Captcha.Store 接口的三个方法:Set、Get、Verify,缺一不可。
常见错误是只写了 Set 和 Get,漏掉 Verify,结果调用 store.Verify(id, answer, true) 时 panic:nil pointer dereference。因为 Verify 方法内部会调用 Get,但如果你没实现它,Go 会用接口零值(nil struct)去调,而 nil struct 的方法调用直接崩溃。
-
Set要带过期时间,例如time.Minute * 2,不能依赖 Redis 默认 TTL -
Get必须判断redis.Nil错误,否则val, err := rdb.Get(ctx, key).Result()在 key 不存在时返回空字符串 +redis.Nil,不判错会导致后续比对永远失败 -
Verify不应重复查库,直接复用Get的逻辑,且 clear 参数要真实传递给Get决定是否删 key
Redis key 命名和过期时间要统一管理
硬编码 "captcha:" + id 看似简单,但实际部署中容易和其他业务 key 冲突,尤其当多个微服务共用一个 Redis 实例时。建议提取成常量,并加命名空间前缀,比如 const captchaKeyPrefix = "auth:captcha:"。
过期时间也不能写死在 Set 方法里。不同场景需要不同有效期:图形验证码通常 2 分钟,短信验证码可能 5 分钟,登录态 token 甚至更长。更好的做法是把 TTL 作为参数传入 Set,或在调用方控制:
立即学习“go语言免费学习笔记(深入)”;
- 图形验证码生成时传
time.Minute * 2 - 短信验证码生成时传
time.Minute * 5 - 避免在
RedisStore结构体里藏一个固定字段,那会锁死所有用法
ctx 不能复用全局变量,必须每次传入
很多示例代码把 context.Background() 提成包级变量 var ctx = context.Background(),这是危险操作。HTTP 请求的 context 带有超时和取消信号,一旦用错,Redis 操作会卡死、积压连接、拖垮整个服务。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
正确做法是:所有 Redis 方法签名都显式接收 context.Context 参数,调用方从 Gin 的 *gin.Context 中取 c.Request.Context() 传下去。例如:
func (r RedisStore) Set(ctx context.Context, id string, value string, ttl time.Duration) error {
key := captchaKeyPrefix + id
return RedisDb.Set(ctx, key, value, ttl).Err()
}
这样既能响应请求中断,也能配合 Gin 的超时中间件(如 gin.Timeout)自动 cancel 后续 Redis 操作。
Verify 后删 key 的时机必须严格匹配业务逻辑
clear 参数不是“验证通过才删”,而是“只要调了 Verify 就按需删”。典型反模式是:用户输错一次,clear=false;再输对了,clear=true —— 这会导致第二次调用 Get 时 key 已被删,比对失败。
真实场景中,绝大多数验证都该设 clear=true,哪怕第一次就输错。原因有二:
- 防止暴力重放:同一个验证码被反复提交
- 避免 Redis key 泄露:输错后 key 还在,攻击者可能枚举 ID 碰撞
唯一例外是“允许重试但限制次数”的场景,这时得自己维护重试计数(存 Redis hash 或 incr),而不是靠保留原始 key。

















