在Web登录场景中,密码从用户输入到落库要经过浏览器、网络和服务端三道关口。很多团队希望借助HTML5提供的能力,在密码离开用户设备前就做一层保护,从而降低传输被截获带来的泄露风险。需要明确的是,前端加密的本质是提升攻击成本,而不是彻底消灭风险,因为前端代码对用户完全透明,任何客户端逻辑都能被逆向分析。

为什么单纯前端处理不够
不少开发者第一反应是写个函数把密码用Base64编码,或者调一次MD5再发给后端。这种做法只是把明文变成了另一串固定字符串,抓包工具照样能拿到这串值,攻击者拿去重放请求就能登录。更糟的是,如果服务端直接拿这个前端哈希当密码校验,等于把哈希值本身变成了“明文密码”。
HTML5并没有提供什么魔法让前端变成可信环境。浏览器里的所有JS都能被用户和攻击者查看、修改。因此前端加密的目标应当是:在HTTPS之外增加一层针对被动监听和简单日志泄露的缓冲,同时把随机盐、派生参数等交给客户端生成,减轻服务端压力,但绝对不能省略服务端的二次哈希与校验。
Web Crypto API做前端派生的可行方案
现代浏览器都支持Web Crypto API,它能在页面内调用底层密码学原语,不会像第三方库那样引入额外依赖。推荐的组合是随机生成盐,再用PBKDF2对密码做多轮派生,输出固定长度的比特串再转十六进制。这样即便同一密码,每次前端算出的密文也不同,能挡住简单的重放。
下面示例展示如何在浏览器里生成盐并派生密钥。注意代码里的特殊字符都已转义,可直接放进页面使用。
// 生成随机盐
async function genSalt(len = 16) {
const buf = new Uint8Array(len);
window.crypto.getRandomValues(buf);
return Array.from(buf).map(b => b.toString(16).padStart(2, '0')).join('');
}
// 使用PBKDF2派生密码
async function derivePassword(password, salt, iterations = 100000) {
const enc = new TextEncoder();
const keyMaterial = await window.crypto.subtle.importKey(
'raw',
enc.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits']
);
const derived = await window.crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: enc.encode(salt),
iterations: iterations,
hash: 'SHA-256'
},
keyMaterial,
256
);
return Array.from(new Uint8Array(derived))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// 用法
(async () => {
const salt = await genSalt();
const hashed = await derivePassword('user_input_pwd', salt);
console.log('salt:', salt);
console.log('hashed:', hashed);
})();
这段代码每次运行都会产生不同的盐,派生结果也随之变化。前端把盐和派生值一起发给后端,后端再将这个派生值当作“待处理密码”,使用自己的盐做第二次PBKDF2或bcrypt,最后存进数据库。即便前端流量被录下,没有后端私盐也无法还原出可用于其他系统的凭证。
三种前端处理思路对比
除了PBKDF2派生,团队常讨论对称加密和非对称加密。对称加密如AES,需要把密钥放前端,等于公开密钥,意义不大;非对称加密用后端公钥加密密码,前端只持公钥,后端私解,能防中间人但挡不住恶意前端脚本。下表列出差异:
| 方案 | 前端持有 | 抗抓包 | 抗恶意前端 | 落地复杂度 |
|---|---|---|---|---|
| PBKDF2派生 | 盐+参数 | 中 | 低 | 低 |
| 对称AES | 密钥 | 低 | 低 | 中 |
| 非对称RSA | 公钥 | 高 | 低 | 高 |
从表格可以看出,如果只防被动泄露,PBKDF2配合HTTPS已足够;如果担心运营商或代理做内容注入,再叠加非对称加密更稳妥,但要把公钥固化到前端并做版本管理。
服务端存储的正确姿势
前端做完派生后,后端收到的并不是原始密码。此时仍需把前端传来的派生串当“半成品”处理。推荐做法是:后端为该用户再生成一份数据库盐,用bcrypt或Argon2对前端派生串做二次哈希,存盐和最终哈希。校验时先取前端派生,再过一遍后端哈希比对。
这样即便数据库被拖库,攻击者拿到的也是双重加盐的结果,无法反推原始密码,也不能直接用到其他网站。同时,前端盐可随登录请求一起提交,后端按用户名取出上次前端盐做一致性校验,防止有人绕过前端直接发弱口令。
常见误区与避坑
第一个误区是认为前端加密后可以明文记日志。任何包含密码派生值的日志都要脱敏,因为派生值在特定系统里就是凭证。第二个误区是把<input>框的type写成password就安全了,这仅影响显示,不影响内存与网络。第三个误区是用localStorage缓存派生密码,本地木马一读便知。
正确做法是登录成功即清掉内存里的密码变量,前端盐只用一次不落盘,传输强制HTTPS并开启HSTS。若用Web Crypto,注意importKey的extractable参数设成false,避免密钥被脚本导出。
小结与落地建议
用HTML5能力加密密码的核心不是让前端变安全屋,而是把风险分散。推荐链路为:页面随机盐加PBKDF2派生,HTTPS传输,后端二次加盐哈希存储,全程不缓存凭证。对普通业务,这套方案性价比最高;对高敏系统,再叠非对称加密和风控设备指纹,把泄露面缩到最小。