You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

前端加密后端解密的安全实现方案咨询(RSA/VueJS/NestJS)

前端加密+后端解密的安全实现方案(VueJS + NestJS)

核心问题修正

你当前的核心隐患不是加密算法本身,而是缺少请求合法性校验——攻击者能拿到明文后用公钥加密发送,本质是服务器无法区分「合法客户端加密的请求」和「攻击者伪造的加密请求」。要实现「客户端仅持公钥,服务器握用户公私钥」的目标,必须结合用户专属密钥对+会话绑定校验+对称加密混合非对称加密的方案,纯RSA因加密长度限制,并不适合直接加密大体积JSON payload。

具体实现步骤

1. 生成用户专属RSA密钥对(后端处理)

  • 用户注册或首次登录时,NestJS后端为该用户生成一对2048位及以上的RSA密钥:公钥返回给前端存储(比如localStorage),私钥用服务器根密钥加密后存入数据库(绝对禁止明文存储私钥)。
  • 每个用户的公钥唯一,而非全局统一,可缩小密钥泄露的影响范围。

2. 混合加密流程(解决RSA加密长度限制)

纯RSA仅能加密小数据(2048位RSA最多加密245字节),因此需结合AES对称加密:

  • 前端Vue侧:
    1. 每次请求前生成随机的AES-256-GCM对称密钥和16字节IV。
    2. 用用户专属RSA公钥加密AES密钥。
    3. 用AES密钥+IV加密整个JSON payload。
    4. 将加密后的AES密钥+加密后的payload+IV+AES加密标签(GCM模式必需)打包发送。
  • 后端NestJS侧:
    1. 从数据库取出该用户的加密私钥,用服务器根密钥解密得到RSA私钥。
    2. 用RSA私钥解密出AES密钥。
    3. 用AES密钥+IV+标签解密payload,得到明文JSON。

3. 增加请求合法性校验(解决伪造请求问题)

这是堵上你所述安全漏洞的关键:

  • 方案一:会话绑定HMAC签名
    • 用户登录后,后端生成一个会话专属随机字符串(nonce),通过HttpOnly Cookie返回给前端。
    • 前端每次请求时,用AES密钥对请求路径+时间戳+nonce生成HMAC-SHA256签名,随加密数据一起发送。
    • 后端验证:检查时间戳是否在5分钟有效窗口内,nonce是否与当前会话匹配,签名是否正确,不通过则直接拒绝请求。
  • 方案二:JWT上下文校验
    • 登录后后端返回包含用户ID、过期时间的JWT,前端将JWT放在Authorization请求头。
    • 后端先验证JWT有效性,再处理加密数据;还可在JWT中加入IP范围等限制,进一步提升安全性。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 01:20:32