前端密码加密实战:从原理到安全存储的完整解决方案

极客彩云 安全 阅读 2,501
赞 16 收藏
二维码
手机扫码查看
反馈

密码加密方案,我为啥现在只用 Argon2

先说结论:我现在新项目一律上 Argon2,老项目能迁就迁。bcrypt 也还行,但已经不够看了。至于那些还在用 SHA-1 拼盐的,真的别闹了。

前端密码加密实战:从原理到安全存储的完整解决方案

其实我也踩过坑。最早做公司后台系统的时候,图省事直接 MD5 加了个固定 salt 存数据库,结果后来安全审计一查,被喷得狗血淋头。改完 bcrypt 又发现登录慢、注册卡,折腾了半天才搞明白参数调优的事儿。再后来听说 Argon2 拿了密码哈希大赛冠军,试了下发现确实更抗 GPU 暴力破解,从那以后就成了我的默认选项。

三种常见方案实操对比

我们主要比三个:bcrypt、PBKDF2 和 Argon2。SHA 类加盐这种原始方式直接淘汰,不值得浪费时间。

better than plain hash: bcrypt

bcrypt 算是老前辈了,Node.js 里有 bcryptjs 或原生 bcrypt,API 很简单:

const bcrypt = require('bcrypt');
const saltRounds = 12;

// 注册时加密
async function hashPassword(password) {
  return await bcrypt.hash(password, saltRounds);
}

// 登录时验证
async function verifyPassword(password, hashed) {
  return await bcrypt.compare(password, hashed);
}

优点很明显:API 干净,自动带 salt,不容易出错。而且 Node.js 原生支持好,很多旧系统都在用。

但我对它越来越不满意。主要是两点:一是抗 GPU 攻击能力弱,二是内存消耗不可控。你调 high rounds 是能拖慢破解速度,但你也把自己服务器搞死了。我之前设成 14,用户注册平均要等 800ms,PM 直接找上门问是不是服务器挂了。

PBKDF2:老派但稳定

PBKDF2 是 Web Crypto API 的标配,浏览器和 Node 都原生支持,不用装包:

async function hashPassword(password, salt) {
  const enc = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    enc.encode(password),
    { name: 'PBKDF2' },
    false,
    ['deriveBits']
  );

  const derivedBits = await crypto.subtle.deriveBits(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: 600000,
      hash: 'SHA-256'
    },
    keyMaterial,
    256
  );

  return Array.from(new Uint8Array(derivedBits))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

这玩意儿最大好处是标准化程度高,FIDO、WebAuthn 都在用。迭代次数拉到 60 万以上也能扛一阵子。

但问题也很明显:代码写起来太啰嗦,一个加密操作整出半屏代码。而且它只依赖 CPU 迭代,不占内存,GPU 并行跑起来依然快。你要真想防得住,得把 iterations 设得极高,用户体验直接崩盘。

再说一句实话:除了合规场景(比如必须符合某标准),我个人不会主动选它。

Argon2:现在的首选

终于说到主角了。Argon2 是 2015 年密码哈希竞赛的 winner,设计目标就是干翻 GPU/ASIC 破解。它有三个参数可调:时间成本、内存成本、并行度。

Node.js 上用 argon2 包(不是 argon2-browser 那个):

const argon2 = require('argon2');

// 默认配置就够用了
const options = {
  type: argon2.argon2id,
  memoryCost: 65536,    // ~64MB
  timeCost: 3,
  parallelism: 1,
  hashLength: 32
};

async function hashPassword(password) {
  return await argon2.hash(password, options);
}

async function verifyPassword(hash, password) {
  try {
    return await argon2.verify(hash, password);
  } catch (err) {
    return false;
  }
}

我一般就用这个配置,99% 场景都够用。注册耗时控制在 300ms 左右,用户无感,攻击者哭死。

它最大的优势是“内存硬”——你想暴力破解?先准备几十 GB 内存。比起纯 CPU 耗时的方案,性价比差太多了。而且官方推荐的参数组合很合理,不像 bcrypt 那样靠猜。

唯一小问题是需要 native binding,Docker 构建时偶尔会因为 node-gyp 报错卡住。不过只要锁定版本、预编译二进制,基本没大问题。

谁更灵活?谁更省事?

论省事:bcrypt > Argon2 > PBKDF2

论安全:Argon2 > bcrypt ≈ PBKDF2(高 iteration 下)

论灵活性:Argon2 完胜,三个维度都能调,适配不同设备和需求。

举个例子:如果你做个移动端 App 后端,想让低端手机也能快速注册,可以把 Argon2 的 memoryCost 调低一点,timeCost 补回来。而 bcrypt 只能调 rounds,一条腿走路。

还有个小细节很多人忽略:Argon2 支持 secret key(类似 pepper)。你可以把一个外部密钥传进去,即使数据库泄露,没有 key 也解不出来。这个功能在金融类应用里特别实用。

踩坑提醒:这三点一定注意

  • 不要自己实现 salt 逻辑。不管是哪种方案,确保 salt 是随机生成且每次不同。我见过有人用用户名当 salt,笑死。
  • 前端永远不做完整哈希。有人为了“减轻服务端压力”,在 JS 里先 hash 一次再提交,等于明文传密码。真要前置处理,也只能做一轮轻量级 digest,重活留给后端。
  • 记得留升级通道。比如现在用 bcrypt,未来想切 Argon2,要在 verify 时判断 hash 前缀($2a$ vs $argon2id$),自动重新加密。不然老用户永远卡在弱算法上。

我自己就在迁移脚本里栽过一次:忘了触发 rehash,导致部分用户登录失败。后来加了日志监控,发现异常立刻报警。

我的选型逻辑

看场景吧,但我有一套固定套路:

  • 新项目 → 无脑 Argon2,配置抄默认就行。
  • 老项目维护 → 能升 Argon2 就升,不能的话至少保证 bcrypt rounds ≥ 12。
  • 合规要求明确 → 按文档来,通常是 PBKDF2 + SHA-256 + 高 iteration。
  • 嵌入式或资源极受限环境 → 才考虑降级到 bcrypt,还得压测性能。

另外提一嘴传输层:不管你后端怎么哈希,前端传密码一定走 HTTPS。我在本地开发时习惯用 fetch('https://jztheme.com/api/login', { method: 'POST', body: JSON.stringify({ password }) }) 测试接口,但上线前必检查协议是不是 https。

还有人问要不要双因素。我的建议是:管理后台、金融类系统必须上;普通 C 端产品看风险收益比。加一层 MFA 固然安全,但也可能劝退一半用户。

总结:技术没有银弹,但有更优解

bcrypt 不是不能用,但它已经是“还能打”的老兵了。Argon2 才是当前综合最优解——更强的安全性、合理的性能开销、足够的灵活性。

当然,没有哪个方案能防住所有攻击。社工库撞库、键盘记录器、中间人劫持……哈希只是防线之一。但至少,别让用户密码以可逆形式躺在你的数据库里。

以上是我踩坑后的总结,希望对你有帮助。有不同看法欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论