POST://crypto-hash-salt-hmac
带点橘子味的馒头的头像
带点橘子味的馒头

FRONTEND / DESKTOP DEV

返回文章列表

密码学入门:哈希、盐与 HMAC 的工程实践

1 分钟· 653 74 次阅读

给博客做了"加密文章"功能,密码怎么存成了第一个安全问题。这篇从工程角度讲清楚哈希、盐、HMAC 三者的区别与正确用法。

哈希:单向的指纹

哈希函数是单向的:sha256("123456") 很容易算,反过来几乎不可能。

const crypto = require("node:crypto");
const hash = crypto.createHash("sha256").update("123456").digest("hex");

但直接存哈希有个致命问题:彩虹表。攻击者预先算好常见密码的哈希表,撞库即可。"123456" 的 sha256 在彩虹表里一查就有。

盐:让每个密码都独一无二

盐(salt)就是给密码加一段随机字符串再哈希:

const salt = crypto.randomBytes(16).toString("hex");
const hash = crypto
  .createHash("sha256")
  .update(`${salt}:${password}`)
  .digest("hex");
// 存储格式:{ salt, hash }

因为盐是随机的,同一个密码在不同用户那里哈希完全不同——彩虹表直接失效。验证时用存的盐重新拼一次再比对:

function verify(password, record) {
  const h = crypto
    .createHash("sha256")
    .update(`${record.salt}:${password}`)
    .digest("hex");
  return h === record.hash;
}

HMAC:带密钥的哈希

HMAC(Hash-based Message Authentication Code)是"哈希 + 密钥"的组合:HMAC-SHA256(secret, message)

它和"盐+哈希"的区别在于语义:

用途 工具 关键点
存密码 盐 + 哈希 防彩虹表
防篡改(token/签名) HMAC + 密钥 无密钥无法伪造

HMAC 的典型工程场景是签名 token。比如给"已解锁原文"的用户发短期凭证:

// 生成:slug + 过期时间 + 签名
const payload = `${slug}:${Date.now() + 30 * 60 * 1000}`;
const sig = crypto.createHmac("sha256", SECRET).update(payload).digest("hex");
const token = `${payload}:${sig}`;

// 验证:重新签名比对 + 检查过期
function verify(slug, token) {
  const [slugPart, exp, sig] = token.split(":");
  if (slugPart !== slug || Date.now() > Number(exp)) return false;
  const expect = crypto.createHmac("sha256", SECRET).update(`${slugPart}:${exp}`).digest("hex");
  return sig === expect;
}

HMAC 的核心价值:只有持有密钥的人才能签发合法 token,篡改任何一个字段都会导致签名不匹配。

工程里的常见错误

错误 后果 正解
明文存密码 脱裤即泄露 盐 + 哈希
不加盐直接哈希 彩虹表秒破 每用户随机盐
用 MD5/SHA1 已被攻破 SHA-256 起
用普通哈希做 token 签名 无密钥,可伪造 用 HMAC
密码强度限制太少 弱密码被撞 结合盐 + 合理策略

小结

一句话总结三者的分工:

  • 哈希——把密码变成不可逆的指纹;
  • ——让每个指纹独一无二,干掉彩虹表;
  • HMAC——用密钥给数据签名,防止篡改与伪造。

存密码用「盐+哈希」,发凭证用「HMAC」,这两条规则够覆盖绝大多数中小项目的安全需求。

相关文章