不能在 Gin 中间件里用 math/rand 生成 token,因其伪随机性导致种子可预测、goroutine 冲突、token 可复现或批量预测,直接绕过认证;应改用 crypto/rand.Read() 并严格检查错误、预分配切片、避免模偏差。

为什么不能在 Gin 中间件里用 math/rand 生成 token
因为 math/rand 是伪随机,种子一旦可预测(比如用 time.Now().UnixNano()),整个序列就能被复现。容器启动、CI 构建、高并发下多个 goroutine 可能拿到相同种子,导致 session ID、reset token 等重复或可批量预测——这不是“偶尔出错”,而是直接绕过认证逻辑。
- 典型错误:中间件里写
rand.New(rand.NewSource(time.Now().UnixNano())).Intn(1000000)生成验证码 - 后果:攻击者抓几个 token 就能推断出后续值,或批量爆破重置链接
- 兼容性陷阱:
math/rand在所有 Go 版本都存在,但“存在”不等于“可用”
crypto/rand.Read() 在 Gin 中间件里的正确调用姿势
crypto/rand.Read() 是唯一可靠入口,但它不是“拿来就用”的函数——它对切片长度、错误检查、调用频次都很敏感。
- 必须提前分配好目标长度的切片:
buf := make([]byte, 32),传nil或make([]byte, 0, 32)会 panic - 必须检查
err:虽然 Linux/macOS 下极少失败,但在某些容器环境或 Windows 上可能返回io.ErrUnexpectedEOF或io.EOF - 别循环小块调用:例如反复
rand.Read([]byte{0})填满 32 字节,这放大系统调用开销,还掩盖单次失败风险 -
rand.Reader是全局变量、线程安全,中间件里直接用,无需封装或加锁
生成 URL-safe 随机字符串并安全注入到 gin.Context
常见需求是生成 API key、密码重置 token 等字符串,不能靠 fmt.Sprintf 拼接或手动映射字符集——易引入模偏差、截断、大小写混淆等问题。
- 推荐路径:先用
crypto/rand.Read(buf)获取字节流 → 再用base64.URLEncoding.EncodeToString(buf)编码 → 最后按需截取(如[:16]) - 截断不影响安全性:Base64 编码后前 N 位仍保持原始熵,只要原始字节数够(如 16 字节 ≈ 128 bit 熵)
- 注入上下文用
c.Set("token", s),后续 handler 用c.Get("token")安全获取,类型断言时注意判空 - 避免写成
c.Writer.Header().Set("X-Token", s):那是响应头,不是请求上下文,下游 handler 读不到
生成安全整数(比如验证码、范围 ID)时别踩 big.Int 坑
crypto/rand 没有 Intn(),必须用 rand.Int(rand.Reader, max),返回的是 *big.Int——这不是设计缺陷,而是防止开发者误用模运算引入分布偏差。
- 错误示范:
int(rand.Read(buf)) % 100:既绕过拒绝采样逻辑,又因buf长度不足或截断引发 panic - 正确做法:
max := big.NewInt(100); n, err := rand.Int(rand.Reader, max),然后转成int64或int(确认范围不溢出) - 注意:Go 1.22+ 提供了
rand.Uint64(),更简洁,但仅限 uint64;超出范围仍得走big.Int - 别自己拼:
binary.BigEndian.Uint64(buf) % 100—— 256 % 100 有偏差,且 buf 不足 8 字节时 panic
crypto/rand.Read() 的错误检查常被简化为 _, _ = rand.Read(buf),尤其在中间件快速原型阶段。一旦容器内熵池临时枯竭(比如某些轻量级镜像或旧版 Kubernetes),token 就可能只填了一半却悄然通过,后续 hex 或 base64 编码时行为不可控——这不是理论风险,是真实发生过的线上事故。


















