必须用crypto/rand而非math/rand生成验证码,因其是密码学安全的真随机数生成器,读取系统熵池且不可预测;math/rand为伪随机、种子可预测,高并发下易重复,导致验证码被爆破或重放。

为什么不能用 math/rand 生成验证码字符串
因为 math/rand 是伪随机,种子可预测,高并发下极易重复——比如容器启动瞬间多个 goroutine 共享同一个 time.Now().UnixNano(),导致生成相同验证码。注册/重置流程一旦被爆破或重放,等于绕过安全防线。
必须用 crypto/rand:它直接读取操作系统熵源(Linux 的 /dev/urandom),线程安全、无需 seed、满足密码学强度。
- 错误写法:
rand.Intn(1000000)→ 可能输出42,不是 6 位;且不安全 - 正确路径:读 3 字节 → 转
uint32→ 取低 20 位 →% 1000000→fmt.Sprintf("%06d", n) - 字符集推荐用
"ABCDEFGHJKLMNPQRSTUVWXYZ23456789",显式排除易混淆字符(0/O、1/l)
base64Captcha.GenerateCaptcha() 返回的 ID 不是凭证
ID 只是服务端存储答案时用的键(如 "f8a3e7b1"),本身无加密、无签名、无绑定。前端拿到后若不做限制,攻击者可无限调用验证接口。
真正要保护的是答案(如 "K7mN"),它必须存进带 TTL 的共享存储,且生命周期严格对齐业务需求。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
- Redis 存储命令示例:
SET captcha:f8a3e7b1 "K7mN" EX 120(2 分钟过期) - 绝对别用内存 map 存多实例部署场景下的答案——不同进程间不共享,验证必失败
- 若用自定义 store 实现,
Set()和Get()必须加sync.RWMutex,否则并发覆盖答案
VerifyString() 返回 false 的真实原因往往不是逻辑错
90% 的“验证失败”其实是传输或处理环节引入了干扰,而非代码 bug。大小写、空格、编码丢失、类型误判,全在这一环掉链子。
- 前端提交前必须
.trim(),否则用户敲完回车会带尾部空格 - 若配置为大小写敏感,但前端把输入转成了
.toLowerCase(),比对必然失败 - HTTP 请求没设
Content-Type: application/json,Go 的json.Unmarshal会吃掉双引号,传入VerifyString的变成aB3x而非"aB3x" - Redis 中取出来的是
[]byte,直接和string比较永远为 false —— 必须显式转成string(ans)
Docker 或多实例部署时字体与 store 的硬伤点
base64Captcha 默认加载 DejaVuSans,但 Windows/macOS/Linux 字体路径完全不同,Docker 镜像里更默认没有。不预加载、不传绝对路径,运行就 panic。
store 同样如此:本地 DefaultMemStore 在重启或扩缩容后清空,生产环境等同于裸奔。
- Windows 字体路径建议:
C:/Windows/Fonts/msyh.ttc;macOS:/System/Library/Fonts/PingFang.ttc;Linux Docker 镜像需COPY wqy-microhei.ttc /app/fonts/并传绝对路径 - 中文支持必须用
captcha.NewCharacter+driverchinese,NewDigit会直接 panic - store 必须全局单例初始化一次,复用同一个
redis.Store实例,别在 handler 里每次 new

















