前端 SHA-256 + 后端密码校验方案与避坑指南

7次阅读
没有评论

很多同学会遇到这个需求:不想把明文密码直接传给后端,于是选择在前端先做 SHA-256 哈希,再把哈希串提交给后端。但这里有非常多容易踩的安全陷阱,很多人只实现了功能,却留下了登录漏洞。本文把原理、代码、安全方案一并整理出来。

先厘清一个核心概念

SHA-256 是哈希摘要算法,不是加密算法,它不可逆。 很多人的误区:前端 SHA256 = 密码加密,传输就安全了。 ❌ 错误:单纯前端计算 sha256(原始密码) 上传,攻击者抓包拿到这个 64 位十六进制哈希值,直接拿着这个哈希值请求登录接口,就能完成重放攻击,和拿到明文效果几乎一样。

一句话总结:前端 SHA256 只能规避「明文密码裸奔传输」,不能单独用来做密码安全方案,更不能直接作为数据库存储的密码摘要。

推荐落地交互流程(生产可用)

  1. 用户进入登录页面,前端先请求后端,获取本次会话临时随机 salt(一次性盐,不存入数据库)
  2. 用户输入密码,前端计算:clientHash = sha256(原始密码 + sessionSalt)
  3. 前端提交账号 + clientHash 到登录接口,不上传原始密码
  4. 后端根据用户名查询用户,取出数据库中保存的慢哈希(argon2id /bcrypt)
  5. 后端使用 clientHash 作为待校验原文,调用慢哈希校验函数比对结果
  6. 注册流程同理:前端拿到注册时下发的 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')
}

重点安全避坑清单

  1. SHA256 是快速哈希,禁止直接用来存储密码 SHA256 计算速度极快,GPU、彩虹表爆破成本很低。密码持久存储必须选用慢哈希算法:argon2id > bcrypt > PBKDF2。前端 SHA256 只是传输层的防护手段,不能替代后端慢哈希。
  2. 禁止无 salt 裸哈希上传sha256(password) 上传 = 等价于把哈希串当成新密码,重放攻击风险极高,必须增加会话一次性 salt。
  3. 不要把慢哈希放到前端计算 bcrypt、argon2 不适合前端:前端算力有限,并且盐值会暴露在前端,失去慢哈希的安全意义,慢哈希一定放在后端。
  4. HTTPS 是底线 就算加了 salt、前端哈希,HTTP 环境下攻击者依然可以劫持接口、篡改下发的 salt。没有 HTTPS,所有前端哈希加固效果大打折扣。
  5. salt 区分两种,不要混淆
    • 会话临时 salt:登录时下发,前端拼接密码做 sha256,用完丢弃,防重放
    • 持久盐(内置在 bcrypt/argon2 摘要内):存在数据库,用于后端密码校验,不需要单独存盐字段

什么时候才适合用这个方案?

✅ 适合:不想明文密码经过网关日志、中间件日志,希望链路中看不到原始密码的场景 ❌ 不适合:想靠 SHA256 单独保障密码安全、不做 HTTPS、不想加随机 salt 的场景

小结

前端 SHA256 不是万能药,它解决的是「明文密码传输留痕」的问题,而不是密码存储安全。标准安全链路是:HTTPS + 会话临时 salt + 前端 sha256 拼接计算 + 后端 argon2id/bcrypt 慢哈希存储校验。 如果只是简单内部后台,图省事直接裸 SHA256,一定要清楚它的安全短板,不建议对外公网服务使用。

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 0
评论(没有评论)
验证码