不能用 math/rand 生成令牌,因其伪随机性导致种子固定时输出可预测;必须用 crypto/rand 调用系统熵池(如 /dev/urandom)生成密码学安全随机数。

为什么不能用 math/rand 生成令牌
直接调用 rand.Intn() 或 rand.Read() 生成的随机数是伪随机,种子固定时输出完全可预测。攻击者只要知道 seed(比如时间戳),就能复现整个令牌序列。真实场景中,API 密钥、重置令牌、session ID 都必须抗预测,必须用密码学安全的随机源。
正确做法:用 crypto/rand 读取系统熵池
crypto/rand 底层调用操作系统提供的安全随机接口(Linux 的 /dev/urandom,Windows 的 BCryptGenRandom),无需手动管理 seed,输出不可预测。
常见错误写法:
// ❌ 错误:用 math/rand 拼接字符串
r := rand.New(rand.NewSource(time.Now().UnixNano()))
bytes := make([]byte, 32)
for i := range bytes {
bytes[i] = byte(r.Intn(256))
}
正确示例(生成 32 字节 token 并转为 base64 URL 安全编码):
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
import (
"crypto/rand"
"encoding/base64"
)
<p>func generateToken() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
</p>
-
base64.URLEncoding避免+和/,适配 URL 和 JSON 场景 - 务必检查
rand.Read()返回的err—— 理论上系统熵枯竭会返回io.ErrUnexpectedEOF,虽极罕见但不可忽略 - 不要用
fmt.Sprintf("%x", b)转 hex:长度翻倍且无熵增,纯属浪费带宽
需要固定长度字符串(如 16 位字母数字)怎么办
不能对 crypto/rand 输出做简单截断或模运算(会破坏均匀分布)。应使用拒绝采样(rejection sampling)确保每个字符等概率出现:
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
func generateAlnumToken(length int) (string, error) {
b := make([]byte, length)
src := make([]byte, length*2) // 多取一倍,降低重试概率
for i := 0; i
- 模运算本身不引入偏差,但前提是
len(letters)是 2 的幂(如 32、64);否则需拒绝采样,上面代码已实现 - 避免用
strings.Builder+ 循环WriteByte—— 小字符串直接string(b)更快 - 若长度固定且较短(如 6–12 位),优先用 base64 或 hex 编码原始字节,比字符映射更高效、更安全
Web 场景下 token 存储和校验的隐含风险
生成安全 token 只是第一步。实际使用中,以下问题常被忽略:
- HTTP-only Cookie 中存储 token 时,务必设置
Secure和SameSite=Strict(或Lax),防止 CSRF 和中间人窃取 - 数据库存 token 时,**永远不要明文存储** —— 即使是“一次性”重置令牌,也应加盐哈希(如
bcrypt.GenerateFromPassword(tokenBytes, bcrypt.MinCost))后再存 - 生成后立即在服务端绑定上下文(如用户 ID、过期时间、用途标识),避免 token 泛化滥用
- 如果用 Redis 存 token 状态,设置
EXPIRE时间必须与业务逻辑一致,不要依赖客户端传来的过期时间
最易被跳过的点:生成 token 后没做即时失效机制。比如用户换绑邮箱时,旧重置令牌应主动从存储中删除,而不是只靠过期时间硬扛。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










