前后端通信加解密(Web Crypto API )

前端使用 ts,后端使用 node,要求 node版本大于18,本人用的 22

前端 encrypt.ts

ts 复制代码
/**
 * 使用 Web Crypto API 进行 AES-GCM 加密和解密
 * 注意:密钥应该从环境变量或配置中获取,不要硬编码
 */

// 加密密钥(应该是32字节,256位)
// 在实际项目中,这个密钥应该从环境变量或安全配置中获取
const ENCRYPTION_KEY = import.meta.env.VITE_ENCRYPTION_KEY
/**
 * 将字符串密钥转换为 CryptoKey
 */
async function getKey(keyString: string): Promise<CryptoKey> {
  const encoder = new TextEncoder()
  const keyData = encoder.encode(keyString)

  // 确保密钥是32字节(256位)
  const keyArray = new Uint8Array(32)
  const sourceKey = new Uint8Array(keyData.slice(0, 32))
  keyArray.set(sourceKey)

  return crypto.subtle.importKey(
    'raw',
    keyArray,
    {
      name: 'AES-GCM',
      length: 256,
    },
    false,
    ['encrypt', 'decrypt'],
  )
}

/**
 * 加密数据
 * @param data - 要加密的数据(可以是对象或字符串)
 * @returns 加密后的字符串(base64格式,包含iv)
 */
export async function encrypt(data: string | object): Promise<string> {
  try {
    const cryptoKey = await getKey(ENCRYPTION_KEY)

    // 将数据转换为字符串
    const dataString = typeof data === 'string' ? data : JSON.stringify(data)
    const encoder = new TextEncoder()
    const dataBuffer = encoder.encode(dataString)

    // 生成随机 IV(初始化向量)
    const iv = crypto.getRandomValues(new Uint8Array(12))

    // 加密数据
    const encryptedData = await crypto.subtle.encrypt(
      {
        name: 'AES-GCM',
        iv: iv,
      },
      cryptoKey,
      dataBuffer,
    )

    // 将 IV 和加密数据合并
    const combined = new Uint8Array(iv.length + encryptedData.byteLength)
    combined.set(iv, 0)
    combined.set(new Uint8Array(encryptedData), iv.length)

    // 转换为 base64 字符串(使用更安全的方式处理大数组)
    const binaryString = Array.from(combined, (byte) => String.fromCharCode(byte)).join('')
    return btoa(binaryString)
  } catch (error) {
    console.error('加密失败:', error)
    throw new Error('数据加密失败')
  }
}

/**
 * 解密数据
 * @param encryptedData - 加密后的数据(base64格式)
 * @returns 解密后的原始数据(如果是JSON字符串,需要手动解析)
 */
export async function decrypt(encryptedData: string): Promise<string> {
  try {
    const cryptoKey = await getKey(ENCRYPTION_KEY)

    // 从 base64 解码
    const binaryString = atob(encryptedData)
    const combined = new Uint8Array(binaryString.length)
    for (let i = 0; i < binaryString.length; i++) {
      combined[i] = binaryString.charCodeAt(i)
    }

    // 提取 IV 和加密数据
    const iv = combined.slice(0, 12)
    const data = combined.slice(12)

    // 解密数据
    const decryptedData = await crypto.subtle.decrypt(
      {
        name: 'AES-GCM',
        iv: iv,
      },
      cryptoKey,
      data,
    )

    // 转换为字符串
    const decoder = new TextDecoder()
    return decoder.decode(decryptedData)
  } catch (error) {
    console.error('解密失败:', error)
    throw new Error('数据解密失败')
  }
}

/**
 * 加密请求参数(自动处理对象)
 * @param data - 请求参数对象
 * @returns 加密后的字符串
 */
export async function encryptRequestData(data: any): Promise<string> {
  return encrypt(data)
}

/**
 * 解密响应数据(自动解析JSON)
 * @param encryptedData - 加密的响应数据
 * @returns 解密后的对象
 */
export async function decryptResponseData(encryptedData: string): Promise<any> {
  const decrypted = await decrypt(encryptedData)
  try {
    return JSON.parse(decrypted)
  } catch {
    return decrypted
  }
}

前端加解密eg:

ts 复制代码
请求加密
await encryptRequestData(config.data)
响应解密
await decryptResponseData(response.data.data)

后端 encrypt.ts

javascript 复制代码
require("dotenv").config();
const { webcrypto } = require("crypto");

// 在 Node.js < 19.0.0 中,TextEncoder 和 TextDecoder 是全局可用的
// 但为了代码的明确性和兼容性,可以从 'util' 模块导入
const { TextEncoder, TextDecoder } = require("util");

const ENCRYPTION_KEY = process.env.VITE_ENCRYPTION_KEY;

/**
 * 将字符串密钥转换为 CryptoKey
 */
async function getKey(keyString) {
  if (!keyString) {
    throw new Error("加密密钥未配置");
  }
  const encoder = new TextEncoder();
  const keyData = encoder.encode(keyString);

  // 确保密钥是32字节(256位)
  const keyArray = new Uint8Array(32);
  const sourceKey = new Uint8Array(keyData.slice(0, 32));
  keyArray.set(sourceKey);

  return webcrypto.subtle.importKey(
    "raw",
    keyArray,
    {
      name: "AES-GCM",
      length: 256,
    },
    false,
    ["encrypt", "decrypt"]
  );
}

/**
 * 加密数据
 * @param data - 要加密的数据(可以是对象或字符串)
 * @returns 加密后的字符串(base64格式,包含iv)
 */
async function encrypt(data) {
  try {
    const cryptoKey = await getKey(ENCRYPTION_KEY);

    // 将数据转换为字符串
    const dataString = typeof data === "string" ? data : JSON.stringify(data);
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(dataString);

    // 生成随机 IV(初始化向量)
    const iv = webcrypto.getRandomValues(new Uint8Array(12));

    // 加密数据
    const encryptedData = await webcrypto.subtle.encrypt(
      {
        name: "AES-GCM",
        iv: iv,
      },
      cryptoKey,
      dataBuffer
    );

    // 将 IV 和加密数据合并
    const combined = new Uint8Array(iv.length + encryptedData.byteLength);
    combined.set(iv, 0);
    combined.set(new Uint8Array(encryptedData), iv.length);

    // 转换为 base64 字符串(使用更安全的方式处理大数组)
    const binaryString = Array.from(combined, (byte) =>
      String.fromCharCode(byte)
    ).join("");
    return btoa(binaryString);
  } catch (error) {
    console.error("加密失败:", error);
    throw new Error("数据加密失败");
  }
}

/**
 * 解密数据
 * @param encryptedData - 加密后的数据(base64格式)
 * @returns 解密后的原始数据(如果是JSON字符串,需要手动解析)
 */
async function decrypt(encryptedData) {
  try {
    const cryptoKey = await getKey(ENCRYPTION_KEY);

    // 从 base64 解码
    const combined = Buffer.from(encryptedData, "base64");

    // 提取 IV 和加密数据
    const iv = combined.slice(0, 12);
    const data = combined.slice(12);

    // 解密数据
    const decryptedData = await webcrypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: iv,
      },
      cryptoKey,
      data
    );

    // 转换为字符串
    const decoder = new TextDecoder();
    return decoder.decode(decryptedData);
  } catch (error) {
    console.error("解密失败:", error);
    throw new Error("数据解密失败");
  }
}

/**
 * 解密请求参数(自动解析JSON)
 * @param encryptedData - 加密的请求数据
 * @returns 解密后的对象
 */
async function decryptRequestData(encryptedData) {
  const decrypted = await decrypt(encryptedData);
  try {
    return JSON.parse(decrypted);
  } catch {
    return decrypted;
  }
}

/**
 * 加密响应数据(自动处理对象)
 * @param data - 响应数据对象
 * @returns 加密后的字符串
 */
async function encryptResponseData(data) {
  return encrypt(data);
}

// 导出函数
module.exports = {
  encrypt,
  decrypt,
  decryptRequestData,
  encryptResponseData,
};

实例:

javascript 复制代码
后端请求解密:
await decryptRequestData(data);
后端响应加密:
const rpd = await encryptResponseData({
  ...response.data,
 });
// 返回第三方接口的响应
 res.json(rpd);
相关推荐
Jacky-0082 小时前
Node + vite + React 创建项目
前端·react.js·前端框架
CoderYanger3 小时前
前端基础——CSS练习项目:百度热榜实现
开发语言·前端·css·百度·html·1024程序员节
i_am_a_div_日积月累_3 小时前
10个css更新
前端·css
她是太阳,好耀眼i3 小时前
Nvm 实现vue版本切换
javascript·vue.js·ecmascript
倚栏听风雨3 小时前
npm命令详解
前端
用户47949283569153 小时前
为什么我的react项目启动后,dom上的类名里没有代码位置信息
前端·react.js
键盘飞行员3 小时前
Vue3+TypeScript项目中配置自动导入功能,遇到了问题需要详细的配置教程!
前端·typescript·vue
han_4 小时前
前端高频面试题之Vue(初、中级篇)
前端·vue.js·面试
一枚前端小能手4 小时前
📜 `<script>`脚本元素 - 从加载策略到安全性与性能的完整指南
前端·javascript