math/rand生成的随机字符串不能用作token,因其输出完全可预测:只要知道种子(如time.now().unixnano()),攻击者就能复现整个序列;高并发下纳秒级时间戳易重复,导致大量相同字符串;它本就设计为可重现,不适用于安全场景。

为什么math/rand生成的随机字符串不能用作token
因为math/rand输出完全可预测:只要知道种子(比如time.Now().UnixNano()),攻击者就能复现整个序列。哪怕你每次请求都新建rand.New(rand.NewSource(time.Now().UnixNano())),纳秒级时间戳在高并发下极易重复,导致大量请求拿到相同字符串。这不是“不够随机”,而是设计上就允许重现——它本就不该出现在安全上下文中。
crypto/rand生成字符串的正确姿势
别拼接、别取模、别自己转进制。直接读字节再编码:
-
make([]byte, 32)分配缓冲区,长度按需(如32字节对应64字符hex或44字符base64) - 调用
crypto/rand.Read(buf),必须检查返回的err和实际读取长度是否等于len(buf) - 用
hex.EncodeToString(buf)或base64.RawURLEncoding.EncodeToString(buf)转成字符串——前者无歧义,后者更紧凑且URL安全
错误写法包括:fmt.Sprintf("%x", rand.Uint64())(长度不固定)、string(rune(rand.Intn(26)+97))循环拼(非均匀、非安全)、或用math/rand生成字节再base64.StdEncoding(含+//,不适合URL)。
性能差到不能接受吗?其实不是
单次crypto/rand.Read()在现代Linux上耗时约100–300ns,远低于HTTP handler常见开销。真正拖慢的是误用:
- 在tight loop里反复调用
Read()每次只读1字节 → 合并成大buffer一次性读 - 为每个小对象(如每个验证码)都新开goroutine调用
crypto/rand→ 没必要,它天生并发安全,直接复用 - 用
crypto/rand生成游戏内抽卡结果 → 属于杀鸡用牛刀,该换回math/rand
实测显示:批量读1KB比逐字节读快两个数量级,且Go运行时对/dev/urandom做了缓冲优化。
math/rand还能用在哪?明确划清边界
它没被废弃,但适用范围很窄:
- 单元测试中构造可重现的模拟数据(固定种子如
rand.NewSource(42)) - CLI工具临时生成测试ID(
rand.Intn(1000)这种无关紧要的场景) - 图形噪声、蒙特卡洛估算、洗牌用户列表用于前端展示(不涉及隐私或权限)
最容易被忽略的坑是:把rand.Perm(n)打乱后的ID列表当成“不可推断”的结果返回给客户端——只要对方知道你用的时间种子,就能还原原始顺序。安全场景下,没有模糊地带。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











