前端密码加密方案全解析
base64编码解码
window.btoa()
window.atob()
MD5 摘要算法
npm install crypto-js
# 安装 crypto-jsimport md5 from 'crypto-js/md5'const login = async () => {if (!form.value) {console.error('表单引用不存在');return;}try {await form.value.validate();// MD5 加密密码const encryptedPassword = md5(registerData.value.password).toString();const loginData = {username: registerData.value.username,password: encryptedPassword};const result = await userLoginService(loginData);// ...后续处理} catch (error) {// 错误处理}
};
AES 加密
import CryptoJS from 'crypto-js';// 使用 AES 加密密码
const encryptPassword = (password, key) => {return CryptoJS.AES.encrypt(password, key).toString();
};const encryptedPassword = encryptPassword('mypassword123', 'secret-key');axios.post('/api/login', {username: 'user123',password: encryptedPassword
});
RSA 非对称加密(推荐)
RSA 公钥加密(非常常见)
- 用户打开登录页时,前端向服务器请求一个一次性的(Nonce)和对应的RSA公钥。
- 前端用这个公钥对密码(有时还会拼接上Nonce防止重放)进行加密,得到密文。
- 前端将密文和Nonce发送给服务器。
- 服务器用自己保存的RSA私钥解密,得到原始密码。此后,服务器再像正常流程一样,为这个密码进行加盐哈希,然后与数据库中的存储结果比对。
# 安装 jsencrypt
npm install jsencrypt
import JSEncrypt from 'jsencrypt'// 后端提供的公钥
const publicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...
-----END PUBLIC KEY-----`;const encryptPassword = (password) => {const encrypt = new JSEncrypt();encrypt.setPublicKey(publicKey);return encrypt.encrypt(password);
};const login = async () => {if (!form.value) {console.error('表单引用不存在');return;}try {await form.value.validate();// RSA 加密密码const encryptedPassword = encryptPassword(registerData.value.password);const loginData = {username: registerData.value.username,password: encryptedPassword};const result = await userLoginService(loginData);// ...后续处理} catch (error) {// 错误处理}
};
客户端哈希(配合服务器提供的Salt)
注册:
前端请求后端获取盐后端生成盐返回
前端发送用户名、密码=sha256(密码+数据库盐)、数据库盐给后端
后端保存密码和盐到数据库
登录:
前端请求后端获取一个随机盐+当前需要登录的用户的盐(数据库盐)
前端计算哈希: SHA256(密码 + 数据库盐)
再次哈希: SHA256(随机盐 + 第一次哈希)
前端发送用户名、密码(二次哈希的密码)、随机盐到后端
后端使用前端传来的盐和当前用户的数据库密码哈希
然后对比后端的哈希结果是否等于前端传过来的二次哈希密码
注册:
H1 = sha256(密码+数据库盐) = 数据库密码
登录:
H2 = SHA256(随机盐 + SHA256(密码 + 数据库盐))
H2’ = SHA256(随机盐 + 数据库密码)
H2 == H2’
注册
// 前端
const firstHash = sha256(用户明文密码 + 数据库盐);
request.post('/user/register', {username: userData.username,password: firstHash,salt: salt}
// 后端
// 创建用户并保存
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setSalt(salt);
userMapper.insert(user);
登录
// 前端
const frontendSalt = String(tempSalt || '');
// 前端计算哈希: SHA256(密码 + 数据库盐)
const firstHash = hashPassword(password, dbSalt);
// 再次哈希: SHA256(前端盐 + 密码哈希)
const finalHash = hashPassword(frontendSalt + firstHash);return request.post('/user/login', {username: String(userData.username || ''),passwordHash: finalHash,frontendSalt: frontendSalt
});
// 后端使用前端盐进行二次哈希验证
String dbHashPassword = user.getPassword();
String expectedHash = passwordUtil.hashPasswordToHex(frontendSalt, dbHashPassword);
return password.equals(expectedHash);
防重放
生成CLIENT_ID和CLIENT_SECRET
在 Linux/macOS 上使用命令行:
bash
# 生成 CLIENT_ID (32个字符)
openssl rand -hex 16# 生成 CLIENT_SECRET (64个字符,更安全)
openssl rand -hex 32
在 Windows 上使用 PowerShell:
# 查看当前执行策略
Get-ExecutionPolicy# 临时设置为允许本地脚本执行
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser# 或者设置为允许所有脚本(不推荐用于生产环境)
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser
powershell .\generate-keys.ps1
$clientIdBytes = New-Object Byte[] 16
$clientSecretBytes = New-Object Byte[] 32
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create()
$rng.GetBytes($clientIdBytes)
$rng.GetBytes($clientSecretBytes)
$clientId = [System.BitConverter]::ToString($clientIdBytes) -Replace '-',''
$clientSecret = [System.BitConverter]::ToString($clientSecretBytes) -Replace '-',''
Write-Output "VITE_APP_CLIENT_ID=$clientId"
Write-Output "VITE_APP_CLIENT_SECRET=$clientSecret"
生成JWT secret
生成 64 字节的随机密钥(128 个十六进制字符)
# 使用更安全的加密随机数生成器
$bytes = New-Object Byte[] 64
[System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
[System.BitConverter]::ToString($bytes) -Replace '-',''