前后端通信加解密(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);
相关推荐
羽沢311 小时前
ECharts 学习
前端·学习·echarts
LYFlied1 小时前
WebAssembly (Wasm) 跨端方案深度解析
前端·职场和发展·wasm·跨端
七月丶1 小时前
实战复盘:我为什么把 TypeScript 写的 CLI 工具用 Rust 重写了一遍?
前端·后端·rust
over6971 小时前
《闭包、RAG与AI面试官:一个前端程序员的奇幻LangChain之旅》
前端·面试·langchain
_F_y1 小时前
Socket编程TCP
网络·网络协议·tcp/ip
JIngJaneIL1 小时前
基于java+ vue交友系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·交友
苹果酱05671 小时前
解决linux mysql命令 bash: mysql: command not found 的方法
java·vue.js·spring boot·mysql·课程设计
拉不动的猪1 小时前
回顾计算属性的缓存与监听的触发返回结果
前端·javascript·vue.js
karshey2 小时前
【IOS webview】h5页面播放视频时,IOS系统显示设置的icon
前端·ios