欢迎光临
我们一直在努力

哈希计算密码存储安全:加盐哈希与彩虹表攻防

数据库泄露后,用户密码是怎么被破解的?"MD5 加密"真的安全吗?为什么安全审计要求用 bcrypt 而不是 SHA256?本文从明文存储到加盐哈希再到专用密码哈希函数,把密码存储的演进讲清楚。

第一阶段:明文存储(绝对禁止)

CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(50) — 明文存密码
);

一旦数据库泄露,所有用户密码直接暴露。攻击者可以拿着邮箱+明文密码去撞其他网站(密码复用攻击)。

真实案例:2019 年 Facebook 明文存储了数亿用户密码,内部员工可直接查看。2020 年 Twitter 内部工具泄露,同样涉及明文密码。

第二阶段:直接哈希(不安全)

// 注册时
const passwordHash = md5(password)
db.saveUser(username, passwordHash)

// 登录时
const inputHash = md5(inputPassword)
const user = db.findUser(username)
if (inputHash === user.passwordHash) {
// 登录成功
}

看起来安全——数据库里存的是哈希值,泄露了也"解不开"。但问题在于:相同的密码有相同的哈希值。

问题一:彩虹表破解

彩虹表(Rainbow Table)是预先计算好的"密码 → 哈希值"映射表。常见密码(123456、password、qwerty 等)的 MD5/SHA256 值早就被计算好存在表里了。

彩虹表片段(MD5):
123456 → e10adc3949ba59abbe56e057f20f883e
password → 5f4dcc3b5aa765d61d8327deb882cf99
qwerty → d8578edf8458ce06fbc5bb76a58c5ca4
abc123 → e99a18c428cb38d5f260853678922e03
…(上亿条)

数据库泄露后,攻击者拿哈希值去彩虹表里查,几秒钟就能还原出密码。

在线 MD5 解密网站就是这个原理——它们不是"解密"了 MD5,而是在庞大的彩虹表里查到了对应的明文。

问题二:相同密码哈希相同

如果两个用户用了相同的密码,他们的哈希值也相同。攻击者只要破解了一个,就知道了所有相同密码用户的密码。

管理员账户如果用了常见密码,整个系统就危险了。

第三阶段:加盐哈希(大幅提升安全性)

加盐(Salt)是在密码哈希前,拼接一段随机字符串:

hash = MD5(password + salt)

每个用户有不同的 salt,相同密码产生不同哈希:

const crypto = require('crypto')

function hashPassword(password) {
// 生成随机 salt(16 字节 = 32 十六进制字符)
const salt = crypto.randomBytes(16).toString('hex')

// 加盐后计算哈希
const hash = crypto
.createHash('sha256')
.update(password + salt)
.digest('hex')

return { salt, hash }
}

// 用户 A 注册
const userA = hashPassword('password123')
// { salt: 'a1b2c3d4…', hash: 'a7b3c9d2…' }

// 用户 B 注册,密码相同
const userB = hashPassword('password123')
// { salt: 'x8y2z5w1…', hash: 'f2d8a1c7…' }
// 不同的 salt → 完全不同的 hash

Salt 不是密钥,不需要保密,和哈希值一起存在数据库里就行。它的作用是让每个用户的哈希计算不同,攻击者无法用一张彩虹表破解所有用户。

加盐后验证密码

function verifyPassword(inputPassword, salt, storedHash) {
const inputHash = crypto
.createHash('sha256')
.update(inputPassword + salt)
.digest('hex')

return inputHash === storedHash
}

const user = db.findUser(username)
if (verifyPassword(inputPassword, user.salt, user.passwordHash)) {
// 登录成功
}

加盐后的攻击成本

方案破解 1 个密码破解 10000 个密码
无盐 MD5 几秒(查彩虹表) 几秒(批量查)
加盐 MD5 几小时(暴力破解) 10000 × 几小时
加盐 SHA256 几小时(暴力破解) 10000 × 几小时

加盐让批量破解成本乘以用户数。但如果用户密码很弱(123456),暴力枚举仍然很快。

第四阶段:专用密码哈希函数(正确方案)

SHA256 是通用哈希函数,设计目标是快。现代 GPU 每秒可以计算几十亿次 SHA256,暴力破解弱密码只需几秒。

专用密码哈希函数的设计目标是慢——故意让计算一次哈希需要几百毫秒,让暴力破解变得不现实。

bcrypt

const bcrypt = require('bcrypt')

// 注册:哈希密码
const saltRounds = 10 // 成本因子,2^10 = 1024 次迭代
const hash = bcrypt.hashSync('myPassword123', saltRounds)
// $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// 算法/成本/ salt(22字符) hash(31字符)

// 登录:验证密码
bcrypt.compareSync('myPassword123', hash) // true
bcrypt.compareSync('wrongPassword', hash) // false

bcrypt 输出格式解析:

  • $2b$:算法标识(bcrypt)
  • 10$:成本因子(2^10 = 1024 轮)
  • N9qo8uLOickgx2ZMRZoMye:22 字符的 salt(自动生成)
  • IjZAgcfl7p92ldGxad68LJZdL17lhWy:31 字符的哈希值

成本因子可以调整:

  • 因子 10:约 100ms / 次(推荐默认)
  • 因子 12:约 400ms / 次(更安全)
  • 因子 14:约 1.5s / 次(高安全场景)

随着硬件性能提升,可以逐步增大成本因子,保持每次计算约 100ms 左右。

scrypt

scrypt 是内存难函数(Memory-hard Function)——不仅计算慢,还需要大量内存。GPU 虽然算力强,但内存带宽有限,scrypt 让 GPU 加速效果大打折扣。

const scrypt = require('scrypt')

// 配置参数
const params = {
N: 16384, // CPU/内存成本(必须是 2 的幂)
r: 8, // 块大小
p: 1 // 并行度
}

const hash = scrypt.hashSync('myPassword123', params, 64, salt)

Argon2

Argon2 是 2015 年密码哈希竞赛(Password Hashing Competition)的冠军,是目前最推荐的密码哈希方案。

const argon2 = require('argon2')

// 哈希
const hash = await argon2.hash('myPassword123', {
type: argon2.argon2id, // 推荐类型(抗侧信道 + 抗 GPU)
memoryCost: 65536, // 内存成本(64MB)
timeCost: 3, // 时间成本(迭代次数)
parallelism: 4 // 并行度
})

// 验证
const valid = await argon2.verify(hash, 'myPassword123')

Argon2 有三个变体:

  • Argon2d:最大化抗 GPU 攻击,适合加密货币
  • Argon2i:抗侧信道攻击,适合密码哈希
  • Argon2id:两者结合(推荐),先 i 后 d

三种密码哈希函数对比

特性bcryptscryptArgon2
发布年份 1999 2009 2015
设计目标 计算慢 计算慢 + 内存难 全面最优
可调参数 成本因子 N, r, p memory, time, parallelism
抗 GPU 一般 好 最好
标准 事实上的标准 RFC 7914 PHC 冠军
生态支持 最广 较广 逐步普及
推荐场景 通用(最稳妥) 高安全场景 新项目首选

各语言推荐方案

语言推荐方案库
Node.js bcrypt / argon2 bcrypt / argon2
Python bcrypt / argon2 bcrypt / argon2-cffi
Java bcrypt / Argon2 Spring Security BCryptPasswordEncoder / argon2
Go bcrypt / argon2 golang.org/x/crypto/bcrypt
PHP password_hash 内置 password_hash(PASSWORD_BCRYPT)
Ruby bcrypt bcrypt gem

在线练习

理解加盐哈希最直观的方式是亲手算一算。可以使用在线的哈希计算工具,推荐一款在线的免费工具 盘子工具站哈希计算工具 上做这种练习——

第一步:输入 password123,记录 MD5 和 SHA256 结果。 在这里插入图片描述

第二步:输入 password123 + 你的随机salt(比如 password123a1b2c3),再算一次哈希。对比加盐和不加盐的结果——加盐后哈希值完全不同。 在这里插入图片描述

第三步:换一个 salt(比如 password123x8y2z),再算一次。相同密码、不同 salt,产生完全不同的哈希值。这就是加盐的核心意义。 在这里插入图片描述

你还可以做一个彩虹表原理的小实验:用工具计算 123456 的 MD5 值 e10adc3949ba59abbe56e057f20f883e,然后去搜"MD5 解密"网站粘贴这个值——它会立刻返回 123456。这就是彩虹表的威力,也是为什么密码存储必须加盐。

所有计算在浏览器本地完成,不上传服务器,适合做安全实验。

赞(0)
未经允许不得转载:171主机测评 » 哈希计算密码存储安全:加盐哈希与彩虹表攻防
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址