很多同学会遇到这个需求:不想把明文密码直接传给后端,于是选择在前端先做 SHA-256 哈希,再把哈希串提交给后端。但这里有非常多容易踩的安全陷阱,很多人只实现了功能,却留下了登录漏洞。本文把原理、代码、安全方案一并整理出来。
先厘清一个核心概念
SHA-256 是哈希摘要算法,不是加密算法,它不可逆。 很多人的误区:前端 SHA256 = 密码加密,传输就安全了。 ❌ 错误:单纯前端计算 sha256(原始密码) 上传,攻击者抓包拿到这个 64 位十六进制哈希值,直接拿着这个哈希值请求登录接口,就能完成重放攻击,和拿到明文效果几乎一样。
一句话总结:前端 SHA256 只能规避「明文密码裸奔传输」,不能单独用来做密码安全方案,更不能直接作为数据库存储的密码摘要。
推荐落地交互流程(生产可用)
- 用户进入登录页面,前端先请求后端,获取本次会话临时随机 salt(一次性盐,不存入数据库)
- 用户输入密码,前端计算:
clientHash = sha256(原始密码 + sessionSalt) - 前端提交账号 + clientHash 到登录接口,不上传原始密码
- 后端根据用户名查询用户,取出数据库中保存的慢哈希(argon2id /bcrypt)
- 后端使用
clientHash作为待校验原文,调用慢哈希校验函数比对结果 - 注册流程同理:前端拿到注册时下发的 salt,算出 clientHash 传给后端,后端再对 clientHash 做 argon2id/bcrypt 慢哈希入库
✅ 关键点:
- 临时 salt 每次登录都不一样,用完即弃,抵御重放
- 数据库里永远不存明文、不存裸 SHA256,只存慢哈希
- 全站必须启用 HTTPS,否则 salt 和请求依然会被劫持篡改
极简方案(仅适合内部测试、演示,严禁生产) 前端直接 sha256 (密码) 上传,后端直接和库内 sha256 字符串全等对比。这个方案功能能跑,但是安全缺陷很大,下文会给出对应代码,仅作演示。
前端实现:Web Crypto 原生 SHA256(无第三方库)
浏览器自带 crypto.subtle,不需要引入 crypto-js,性能更好。
/**
* 计算字符串 SHA256,返回小写十六进制串,和后端结果对齐
* @param {string} str 待哈希字符串
* @returns {Promise<string>} sha256 hex
*/
async function sha256(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
// 登录示例
async function login() {
// 1. 先获取本次会话临时salt
const saltRes = await fetch("/api/login/salt");
const { sessionSalt } = await saltRes.json();
const rawPwd = document.getElementById("password").value;
// 2. 密码 + 临时salt 做sha256
const clientHash = await sha256(rawPwd + sessionSalt);
// 3. 提交哈希串,不提交明文密码
const resp = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username: document.getElementById("username").value,
passwordHash: clientHash
})
});
const result = await resp.json();
console.log(result);
}
如果项目已经使用 crypto-js:
import CryptoJS from 'crypto-js'
const hash = CryptoJS.SHA256("123456").toString(CryptoJS.enc.Hex)
后端实现代码
说明:后端 SHA256 代码仅用于【极简演示方案】;生产环境后端核心校验依赖 bcrypt /argon2id,而不是直接比对 sha256。
Java(SpringBoot,JDK 原生)
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class HashUtil {
/**
* SHA256 小写hex,和前端WebCrypto结果对齐
*/
public static String sha256(String content) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(content.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}
SpringBoot 生产推荐:使用 BCryptPasswordEncoder,将前端传过来的 clientHash 当做原始密码做校验。
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 校验:encoder.matches(clientHash, dbBcryptHash)
Go 版本
package util
import (
"crypto/sha256"
"encoding/hex"
)
func Sha256(s string) string {
h := sha256.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}
Python 版本
import hashlib
def sha256_hex(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
Node.js 后端
const crypto = require('crypto')
function sha256(str) {
return crypto.createHash('sha256').update(str, 'utf8').digest('hex')
}
重点安全避坑清单
- SHA256 是快速哈希,禁止直接用来存储密码 SHA256 计算速度极快,GPU、彩虹表爆破成本很低。密码持久存储必须选用慢哈希算法:
argon2id > bcrypt > PBKDF2。前端 SHA256 只是传输层的防护手段,不能替代后端慢哈希。 - 禁止无 salt 裸哈希上传
sha256(password)上传 = 等价于把哈希串当成新密码,重放攻击风险极高,必须增加会话一次性 salt。 - 不要把慢哈希放到前端计算 bcrypt、argon2 不适合前端:前端算力有限,并且盐值会暴露在前端,失去慢哈希的安全意义,慢哈希一定放在后端。
- HTTPS 是底线 就算加了 salt、前端哈希,HTTP 环境下攻击者依然可以劫持接口、篡改下发的 salt。没有 HTTPS,所有前端哈希加固效果大打折扣。
- salt 区分两种,不要混淆
- 会话临时 salt:登录时下发,前端拼接密码做 sha256,用完丢弃,防重放
- 持久盐(内置在 bcrypt/argon2 摘要内):存在数据库,用于后端密码校验,不需要单独存盐字段
什么时候才适合用这个方案?
✅ 适合:不想明文密码经过网关日志、中间件日志,希望链路中看不到原始密码的场景 ❌ 不适合:想靠 SHA256 单独保障密码安全、不做 HTTPS、不想加随机 salt 的场景
小结
前端 SHA256 不是万能药,它解决的是「明文密码传输留痕」的问题,而不是密码存储安全。标准安全链路是:HTTPS + 会话临时 salt + 前端 sha256 拼接计算 + 后端 argon2id/bcrypt 慢哈希存储校验。 如果只是简单内部后台,图省事直接裸 SHA256,一定要清楚它的安全短板,不建议对外公网服务使用。