安全且不卡死的scrypt参数为:N=131072(2¹⁷)、r=8、p=1,此时耗时约80–150ms、内存约16MB;salt不可为nil,须独立存储n/r/p/salt/key_len,升级时需支持双版本验证。

scrypt 参数怎么选才安全又不卡死
直接说结论:scrypt.Key 的 N、r、p 三参数不能随便填,填错要么被暴力破解,要么在用户注册时卡住几秒甚至 OOM。默认示例里的 N=32768(即 2¹⁵)是 2017 年的保守值,现在建议至少 N=131072(2¹⁷),r=8,p=1 —— 这组在普通服务器上耗时约 80–150ms,内存占用 ~16MB,平衡了安全性与响应体验。
常见错误现象:runtime: out of memory 或 HTTP 请求超时,基本都是 N 设太高(比如误用 1)、<code>r 和 p 搭配失衡(如 r=16, p=4 导致内存翻倍再翻倍)。scrypt 内存占用 ≈ 128 * r * N 字节,务必按公式心算一下。
-
N必须是 2 的幂,推荐范围:65536~524288(低于 32768 已不推荐) -
r控制块大小,影响内存带宽压力,8是当前广泛验证过的稳妥值 -
p是并行度,生产环境强烈建议固定为1;设为 >1 会线性增加内存且不提升抗 ASIC 能力 - 最终密钥长度(
keyLen)按需求定,AES-256 就传32,加盐哈希存储就传32或64
盐值(salt)必须每次随机生成,不能复用或硬编码
很多人把 salt 写成常量字符串或用时间戳,这会让相同密码永远派生出相同密钥,完全丧失 salt 意义。正确做法是调用 crypto/rand.Read 生成强随机字节,长度至少 16 字节(推荐 32)。
使用场景:每次用户注册/改密时,都应生成新 salt,并和派生出的密钥一起存入数据库(salt 明文存,密钥密文存)。验证时从 DB 取出 salt,再用同样参数重算一次密钥比对。
- 别用
math/rand—— 它不是密码学安全的,输出可预测 - 别用用户名、邮箱、ID 做 salt —— 这些是公开信息,等价于没加 salt
- 生成 salt 示例:
var salt [32]byte<br>if _, err := rand.Read(salt[:]); err != nil {<br> return err<br>} - 存 salt 时建议 base64 编码后存字符串字段(如
base64.StdEncoding.EncodeToString(salt[:])),读取时再解码
调用 scrypt.Key 时的常见 panic 和类型陷阱
最常遇到的是 panic: runtime error: makeslice: len out of range,本质是传了负数或超大整数给 keyLen,或者 salt 是 nil 切片。另一个隐性坑是 Go 的 []byte 传递不拷贝底层数组,如果复用同一块内存做多次 scrypt.Key 计算,可能因内部缓冲区覆盖导致结果错乱。
-
scrypt.Key第三个参数(keyLen)必须 > 0 且 ≤ 2³²−1,但实际建议 ≤ 64 -
salt不能是nil,哪怕长度为 0 也要传[]byte{};空 salt ≠ 安全,只是兼容旧逻辑 - 别把
scrypt.Key包进无缓存 goroutine —— 它是 CPU+内存密集型,大量并发会拖垮服务;建议加限流或异步队列 - 错误处理不能忽略:
err != nil时可能是参数越界或系统资源不足,需记录日志并拒绝该次请求
和 bcrypt / Argon2 对比时的关键事实
Go 标准库没内置 bcrypt,x/crypto/bcrypt 是独立包;而 x/crypto/argon2 虽更现代,但需要 Go 1.12+ 且参数更复杂。scrypt 在 Go 生态里仍是“开箱即用、参数可控、审计充分”的务实选择 —— 但它不提供内置的串行化格式(如 bcrypt 的 $2b$12$...),所有参数和 salt 都得自己存、自己传。
这意味着你无法像用 bcrypt 那样一句 bcrpyt.CompareHashAndPassword 完成验证:每次验证都得手动拼接参数、读 salt、调 scrypt.Key、再 bytes.Equal。漏掉任何一环(比如用了旧 salt 或错的 N)就会永久锁死用户。
- 别试图把所有参数塞进一个 base64 字符串里自定义格式 —— 容易解析出错,建议拆成独立数据库字段:
salt、n、r、p、key_len - 升级参数时(比如从
N=32768升到131072),要支持双版本验证逻辑,等用户下次登录再自动重算并更新存储 - Argon2i 更抗 GPU,但 Go 的
argon2.IDKey默认使用方式对内存访问模式不够友好,实际性能未必优于调优后的 scrypt
真正麻烦的从来不是调用一行函数,而是参数管理、salt 生命周期、错误分支覆盖、以及多年后还能否用当时那套参数正确验签 —— 这些细节不写进迁移文档,迟早出事。


















