口令哈希生成与校验

生成可以存进数据库的口令哈希串,或者按哈希串里记录的参数重算一遍校验口令。哈希不是加密,算出结果之后原始口令无法还原。

把一个口令转成可以存进数据库的哈希串。

推荐。成本可调,代价随硬件升级只需调高数字,存量哈希仍然有效。

6 个容易踩的点

哈希不是加密,也不该能解回来。加密是双向的,有密钥就能还原;哈希是单向的,设计目标就是不可逆。所以密码库里存的应该是哈希,系统自己也不知道用户的原始口令是什么。如果某个系统能「把密码发回给你」,那它要么存了明文,要么用了可逆加密,两种都是严重的安全问题。忘记密码的正确流程是重置,不是找回。

快哈希做口令哈希就是灾难。SHA-256 被设计得越快越好,一秒能算上亿次。用裸 SHA-256 存口令,一块普通显卡每秒能试几十亿个候选,八位以内的口令基本当场就出来了。所以口令必须用「故意很慢」的算法:bcrypt 靠成本指数、PBKDF2 靠迭代次数、Argon2 还额外占用大量内存。评价标准很简单——正常登录能接受,批量试错就慢到不可行。

四种格式怎么选。有得选就用 Argon2id,它是 2015 年密码哈希竞赛的获胜者,同时吃时间和内存,抗 GPU 最好。次选 bcrypt,十几年的实战检验,实现到处都是。再次是 scrypt 或 PBKDF2。但要注意:本页不提供 Argon2 和 scrypt,因为浏览器端需要额外引入 WASM 实现才靠谱,用纯 JS 写出来的版本性能和安全性都难以保证——真要用,请在服务端做。sha512-crypt 只在需要写 /etc/shadow 或者兼容老系统时才用。

成本参数要跟着硬件走。bcrypt 的成本每加 1,计算量翻倍。目标是单次校验耗时 100 到 500 毫秒:太低挡不住暴力破解,太高则登录接口会先被自己的用户打垮,登录压力大的系统还容易被这种慢哈希拖成拒绝服务。选定后写进哈希串里,将来硬件升级了就把新用户和登录成功的用户重新哈希一遍,老哈希继续可校验,这就是「登录时自动升级」的常规做法。

盐、胡椒、以及别忘的限流。盐必须每个用户随机且唯一,和哈希一起明文存储,作用是不让相同口令得到相同结果。胡椒(pepper)是额外的一层:在哈希之前把口令和一个全站密钥拼起来,密钥存在代码或配置里而不是数据库里。它的价值在于数据库被拖走时攻击者仍然少一块拼图,但要记得密钥轮换很麻烦,收益也不如直接把 bcrypt 成本调高,所以属于可选加固。另外,哈希再强也挡不住在线撞库——登录接口的失败次数限制、验证码和异地提醒同样是必需品。

迁移旧哈希要能平滑过渡。存量的老哈希(md5、sha1、裸 sha256)没法反推出口令,只能在用户下次登录时顺带升级:先用老算法校验,通过后立刻用新算法重新哈希覆盖,然后在新字段上打标记。这样用户无感,几个月后大部分活跃账号就都迁移完了。不要一次性强制所有人重置密码,那会把一半用户赶走。识别哈希格式时一定要按哈希串的前缀和参数走,不要靠字段长度猜。

各格式长什么样

bcrypt 的哈希串固定 60 个字符,形如 $2b$12$ 加上 22 个字符的盐和 31 个字符的摘要;$2a$、$2b$、$2y$ 三个前缀算法一致,差别只在 72 字节边界和空字节的处理上。sha512-crypt 形如 $6$rounds=10000$盐$摘要,用在 /etc/shadow 里;轮次等于默认 5000 时按惯例省略 rounds=。Django 的 PBKDF2 形如 pbkdf2_sha256$600000$盐$哈希,全部用美元符号分隔,这也是它的好处:算法、迭代次数、盐、摘要都在一个字段里,校验时不需要额外查表。

正因为参数都写在哈希串里,迁移系统才可能平滑:老用户先用老算法校验,通过后立刻用新算法重新哈希并覆盖,用户完全无感。如果把这些参数放在另外的字段里,字段一多就容易错位,也很难支持同一张表里混存多种算法。本工具的校验就是严格按哈希串里记录的参数重算的,所以能直接验证别人系统导出来的存量数据。

在浏览器里算口令哈希靠谱吗

这里算出来的结果和服务器端算的完全一致,可以放心拿来核对、测试和生成测试数据。但生产环境的哈希必须在服务端做,原因有两条:一是浏览器端的耗时完全由用户的机器决定,成本参数失去了意义;二是把哈希逻辑放在前端意味着任何人都能拿到算法和参数,虽然这不影响哈希本身的安全性,但没有必要暴露。