从明文到双重哈希
如何用「SHA-256 + bcrypt」双重哈希加密保护用户密码
之前我的一个项目一直用明文存储用户密码——我知道,这很糟糕。最近终于下定决心做了一次彻底的改造,采用了「前端 SHA-256 + 后端 bcrypt」的双重加密方案。这篇文章就来聊聊这套方案的思路、实现和权衡。
为什么明文存储密码是不可接受的?
道理很简单:一旦数据库泄露,所有用户的密码就彻底暴露了。更糟的是,大多数人在多个平台复用同一个密码,这意味着攻击者可以用你泄露的数据库去撞其他平台,造成连锁伤害。
即便你认为自己的数据库"不会被黑",这也不是一个可以接受的赌注。
整体方案:两道防线
这套方案的核心思路是在两个不同的阶段分别做一次哈希:
用户输入明文密码
│
▼
[前端] SHA-256 哈希 ──── 网络传输 ────▶ [后端] bcrypt 哈希
│ │
▼ ▼
localStorage 数据库存储
(记住密码时) pass_hash字段
两道防线各司其职:
- SHA-256(前端):确保明文密码永远不会出现在网络请求中
- bcrypt(后端):确保即使数据库泄露,攻击者也极难还原出原始哈希
第一道防线:前端 SHA-256
用户在登录框输入密码后,前端立刻用 crypto-js 对其做 SHA-256 哈希,然后把这个哈希值(一个 64 字符的十六进制字符串)发给后端,明文密码自始至终不离开用户的浏览器。
// utils/crypto.ts
import CryptoJS from 'crypto-js';
export function hashPassword(plaintext: string): string {
return CryptoJS.SHA256(plaintext).toString();
}
登录请求发出的是这样的内容:
{
"username": "alice",
"pass_hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92"
}
如果用户勾选了「记住密码」,localStorage 里存的也是这个 SHA-256 值,而非明文。即便用户的本地存储被读取,攻击者拿到的也只是一个哈希。
第二道防线:后端 bcrypt
后端(Rust)接收到 SHA-256 字符串后,并不直接存入数据库,而是再用 bcrypt 对它做一次哈希。
// src/main.rs
use bcrypt::{hash, DEFAULT_COST};
let pass_hash = hash(&sha256_from_frontend, DEFAULT_COST)?;
// 生成类似 "$2b$12$KixG.v88VwR..." 的字符串,存入数据库
bcrypt 有两个关键特性让它非常适合存储密码:
- 内置随机 Salt:每次哈希结果都不同,天然防止彩虹表攻击。即使两个用户的密码完全一样,数据库里存的 hash 也完全不同。
- 计算成本可调:
DEFAULT_COST(通常为 10~12)意味着每次哈希需要消耗一定的计算资源,让暴力破解的代价极高。
登录时的验证也很直接:
use bcrypt::verify;
let is_valid = verify(&sha256_from_frontend, &stored_bcrypt_hash)?;
一个完整的例子
假设用户密码是 123456,完整流程如下:
| 阶段 | 操作 | 结果 |
|---|---|---|
| 用户输入 | 在浏览器输入密码 | 123456 |
| 前端哈希 | SHA256("123456") |
8d969eef...c92(64字符) |
| 网络传输 | 发送给后端 | {"pass_hash": "8d969eef..."} |
| 后端哈希 | bcrypt("8d969eef...") |
$2b$12$KixG.v88VwR... |
| 数据库存储 | 写入 pass_hash 字段 |
$2b$12$KixG.v88VwR... |
| 下次登录 | bcrypt::verify(前端SHA256, 数据库bcrypt) |
匹配 ✓ |
向后兼容:存量账号怎么处理?
改造前已有的账号,数据库里存的可能是明文或纯 SHA-256。强制要求所有用户重置密码体验很差,所以我在登录逻辑里加了静默升级:
用户登录
│
├─ 是 bcrypt 格式? ──▶ 直接 bcrypt::verify,通过则完成
│
├─ 是旧 SHA-256? ──▶ 对比后通过,立刻重新用 bcrypt 存储,替换旧记录
│
└─ 是明文? ──▶ 对比后通过,立刻升级为 bcrypt 格式
用户无感知,每次登录后账号自动升级,存量问题会随着时间自然消解。
这个方案能防住什么?
| 威胁场景 | 防御效果 |
|---|---|
| 网络抓包/中间人 | ✅ 传输的是 SHA-256,明文不暴露 |
| 数据库拖库 | ✅ 存的是 bcrypt,暴力破解成本极高 |
| 彩虹表攻击 | ✅ bcrypt 内置 Salt,查表无效 |
| 多平台撞库 | ✅ 攻击者拿到的 SHA-256 是针对本站的中间值,无法直接用于其他站 |
有没有不足?
坦白说,这套方案并非无懈可击:
- 前端 SHA-256 本身不是「加密」,它是确定性哈希。如果攻击者截获了 SHA-256 值,他可以直接用这个值向服务器发起重放攻击(replay attack)。不过因为上了 HTTPS,截获的可能性就极低了。
- 更严格的方案会在前端加入随机 Salt 或者用 PBKDF2/Argon2 替代 SHA-256,但对于这个项目来说,现有方案的安全性已经够了。
- 本质上,防御纵深越深,实现复杂度越高。这套「SHA-256 + bcrypt」方案在安全性和实现成本之间取了一个合理的平衡点。
从明文到双重哈希,这次改造带给我最大的感受是:密码安全不需要造轮子,用对工具就够了。SHA-256 负责保护传输,bcrypt 负责保护存储,两者各司其职,组合在一起就能覆盖绝大多数的威胁场景。
如果你的项目还在用明文存密码,希望这篇文章能给你一点动力,早改早安心。