前端加密后端解密的安全实现方案咨询(RSA/VueJS/NestJS)
前端加密+后端解密的安全实现方案(VueJS + NestJS)
核心问题修正
你当前的核心隐患不是加密算法本身,而是缺少请求合法性校验——攻击者能拿到明文后用公钥加密发送,本质是服务器无法区分「合法客户端加密的请求」和「攻击者伪造的加密请求」。要实现「客户端仅持公钥,服务器握用户公私钥」的目标,必须结合用户专属密钥对+会话绑定校验+对称加密混合非对称加密的方案,纯RSA因加密长度限制,并不适合直接加密大体积JSON payload。
具体实现步骤
1. 生成用户专属RSA密钥对(后端处理)
- 用户注册或首次登录时,NestJS后端为该用户生成一对2048位及以上的RSA密钥:公钥返回给前端存储(比如localStorage),私钥用服务器根密钥加密后存入数据库(绝对禁止明文存储私钥)。
- 每个用户的公钥唯一,而非全局统一,可缩小密钥泄露的影响范围。
2. 混合加密流程(解决RSA加密长度限制)
纯RSA仅能加密小数据(2048位RSA最多加密245字节),因此需结合AES对称加密:
- 前端Vue侧:
- 每次请求前生成随机的AES-256-GCM对称密钥和16字节IV。
- 用用户专属RSA公钥加密AES密钥。
- 用AES密钥+IV加密整个JSON payload。
- 将
加密后的AES密钥+加密后的payload+IV+AES加密标签(GCM模式必需)打包发送。
- 后端NestJS侧:
- 从数据库取出该用户的加密私钥,用服务器根密钥解密得到RSA私钥。
- 用RSA私钥解密出AES密钥。
- 用AES密钥+IV+标签解密payload,得到明文JSON。
3. 增加请求合法性校验(解决伪造请求问题)
这是堵上你所述安全漏洞的关键:
- 方案一:会话绑定HMAC签名
- 用户登录后,后端生成一个会话专属随机字符串(nonce),通过HttpOnly Cookie返回给前端。
- 前端每次请求时,用AES密钥对
请求路径+时间戳+nonce生成HMAC-SHA256签名,随加密数据一起发送。 - 后端验证:检查时间戳是否在5分钟有效窗口内,nonce是否与当前会话匹配,签名是否正确,不通过则直接拒绝请求。
- 方案二:JWT上下文校验
- 登录后后端返回包含用户ID、过期时间的JWT,前端将JWT放在
Authorization请求头。 - 后端先验证JWT有效性,再处理加密数据;还可在JWT中加入IP范围等限制,进一步提升安全性。
- 登录后后端返回包含用户ID、过期时间的JWT,前端将JWT放在
4. 前端代码示例(VueJS)
import { JSEncrypt } from 'jsencrypt'; import CryptoJS from 'crypto-js'; // 加密请求数据 async function encryptRequest(payload, publicKey, nonce) { // 生成AES密钥和IV const aesKey = CryptoJS.lib.WordArray.random(32).toString(); const iv = CryptoJS.lib.WordArray.random(16).toString(); // RSA加密AES密钥 const rsaEncryptor = new JSEncrypt(); rsaEncryptor.setPublicKey(publicKey); const encryptedAesKey = rsaEncryptor.encrypt(aesKey); // AES加密payload(GCM模式) const aesEncryptResult = CryptoJS.AES.encrypt( JSON.stringify(payload), CryptoJS.enc.Utf8.parse(aesKey), { iv: CryptoJS.enc.Utf8.parse(iv), mode: CryptoJS.mode.GCM } ); const encryptedPayload = `${aesEncryptResult.ciphertext.toString(CryptoJS.enc.Base64)}.${aesEncryptResult.authTag.toString(CryptoJS.enc.Base64)}`; // 生成HMAC签名 const timestamp = Date.now().toString(); const signStr = `${window.location.pathname}${timestamp}${nonce}`; const signature = CryptoJS.HMACSHA256(signStr, aesKey).toString(); return { encryptedAesKey, encryptedPayload, iv, timestamp, signature, nonce }; } // 发送加密请求 async function sendEncryptedRequest(url, payload) { const publicKey = localStorage.getItem('userPublicKey'); const nonce = document.cookie.split('; ').find(row => row.startsWith('sessionNonce=')).split('=')[1]; const encryptedData = await encryptRequest(payload, publicKey, nonce); await axios.post(url, encryptedData, { headers: { 'Authorization': `Bearer ${localStorage.getItem('jwtToken')}` } }); }
5. 后端代码示例(NestJS)
import * as crypto from 'crypto'; import { Injectable, UnauthorizedException } from '@nestjs/common'; @Injectable() export class EncryptionService { // 从数据库获取并解密用户私钥 private async getUserPrivateKey(userId: string): Promise<string> { // 实际逻辑:从DB取出加密私钥,用服务器根密钥解密 return '-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'; } // 从Redis获取会话nonce private async getSessionNonce(userId: string): Promise<string> { // 实际逻辑:根据userId从Redis取当前会话的nonce return 'xxx-session-nonce-xxx'; } async decryptRequest(encryptedData: any, userId: string, requestPath: string): Promise<any> { const { encryptedAesKey, encryptedPayload, iv, timestamp, signature, nonce } = encryptedData; // 验证时间戳有效性 if (Date.now() - parseInt(timestamp) > 5 * 60 * 1000) { throw new UnauthorizedException('请求已过期'); } // 验证会话nonce const sessionNonce = await this.getSessionNonce(userId); if (nonce !== sessionNonce) { throw new UnauthorizedException('无效会话验证信息'); } // RSA解密AES密钥 const privateKey = await this.getUserPrivateKey(userId); let aesKey: string; try { aesKey = crypto.privateDecrypt( { key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(encryptedAesKey, 'base64') ).toString(); } catch (err) { throw new UnauthorizedException('密钥解密失败'); } // 验证HMAC签名 const signStr = `${requestPath}${timestamp}${nonce}`; const computedSignature = crypto.createHmac('sha256', aesKey) .update(signStr) .digest('hex'); if (computedSignature !== signature) { throw new UnauthorizedException('请求签名验证失败'); } // AES解密payload const [ciphertextB64, tagB64] = encryptedPayload.split('.'); const decipher = crypto.createDecipheriv( 'aes-256-gcm', Buffer.from(aesKey, 'utf8'), Buffer.from(iv, 'utf8') ); decipher.setAuthTag(Buffer.from(tagB64, 'base64')); let decryptedPayload = decipher.update(ciphertextB64, 'base64', 'utf8'); decryptedPayload += decipher.final('utf8'); return JSON.parse(decryptedPayload); } }
额外安全建议
- 前端核心加密逻辑可封装进WebAssembly模块,增加逆向破解难度。
- 定期轮换用户RSA密钥对(比如用户修改密码时重新生成)。
- 所有请求必须走HTTPS,防止公钥被篡改及中间人攻击。
- 前端避免在控制台输出加密密钥、明文数据等敏感信息。
内容的提问来源于stack exchange,提问作者psjason
相关产品推荐
相关产品推荐

