前端安全接口加密实战:基于AES-CBC的参数_返回值加密方案
2024年10月4日
安全接口加密实战:基于AES-CBC的参数_返回值加密
前端接口加密实战:基于AES-CBC的参数/返回值加密方案
附完整TypeScript代码+调用示例+密钥拆分逻辑,解决接口数据传输安全问题
在前后端数据交互过程中,接口参数和返回值的加密是保障数据安全的重要手段。本文基于 CryptoJS 实现 AES-CBC 模式的加解密方案,结合 SessionId 动态生成密钥/偏移量,完成前端接口参数加密、返回值解密的全流程封装,适用于 Vue3 + Pinia 技术栈项目。实现该方案需先安装以下依赖:
# 安装CryptoJS(AES加解密核心库)
npm install crypto-js --save
# 安装js-base64(Base64编码/解码)
npm install js-base64 --save
首先是密钥/偏移量处理工具(tools.ts),该文件主要实现从用户 SessionId 拆分 AES 所需的 key/iv,以及 Base64 编解码工具函数,代码全程带详细注释:
// 引入Base64编码/解码库,用于密钥的Base64转换
import { Base64 } from 'js-base64';
// 引入项目基础配置(保留原代码,可根据实际项目调整)
import { BaseKey } from '/@/config/base';
// 引入本地存储工具(保留原代码,可根据实际项目调整)
import { Local, Session } from '/@/utils/storage';
// 引入pinia相关工具,用于获取用户状态
import { storeToRefs } from 'pinia';
// 引入用户信息仓库,用于获取sessionId
import { useUserInfo } from '/@/stores/userInfo';
/**
* @description 将密钥转换为Base64编码
* @param {any} scret 原始密钥字符串
* @return {*} Base64编码后的密钥(失败返回undefined)
*/
export const getCryptoKeyToBase64 = (scret: any) => {
try {
// 边界判断:密钥存在才执行转换
if (scret) {
// 使用js-base64库将原始密钥转为Base64编码
const base = Base64.encode(scret);
return base;
}
} catch (error) {
// 捕获转换异常,输出警告日志
console.warn(`密钥Base64编码失败`, error);
}
};
/**
* @description 将Base64编码的密钥解密为原始字符串
* @param {any} scret Base64编码的密钥
* @return {*} 原始密钥字符串(失败返回undefined)
*/
export const getCryptoKeyParse = (scret: any) => {
try {
// 边界判断:Base64密钥存在才执行解析
if (scret) {
// 使用js-base64库将Base64密钥解析为原始字符串
return Base64.decode(scret);
}
} catch (error) {
// 捕获解析异常,输出警告日志
console.warn(`密钥Base64解析失败`, error);
}
};
/**
* @description 从用户sessionId中拆分出AES加密所需的key和iv
* @return {*} { key: 前16位字符串, iv: 后16位字符串 }
*/
export const TokenSplitHandle = () => {
try {
// 获取pinia中用户信息仓库实例
const stores = useUserInfo();
// 解构出用户信息响应式数据(storeToRefs保持响应式)
const { userInfos } = storeToRefs(stores);
// 边界判断:用户信息不存在则直接返回
if (!userInfos.value) return;
// 从用户信息中获取sessionId(用于生成key和iv)
const sessionId: any = userInfos.value?.sessionId;
// 将sessionId中的"-"替换为空(避免特殊字符影响加解密)
const sessionIdStr = sessionId.replace(/-/g, '');
if (sessionIdStr) {
// 截取sessionId前16位作为AES加密的key(AES-128要求key为16位)
const sessionIdStrStart = sessionIdStr.substring(0, 16);
// 截取sessionId后16位作为AES加密的iv(CBC模式要求iv为16位)
const sessionIdStrEnd = sessionIdStr.substring(sessionIdStr.length - 16, sessionIdStr.length);
// 返回key和iv供加密解密使用
return {
key: sessionIdStrStart,
iv: sessionIdStrEnd,
};
}
// 若sessionId处理失败,弹出提示(适配多语言可替换为i18n)
alert('Get key iv failed');
} catch (error) {
// 捕获异常,避免程序崩溃
console.error('拆分sessionId获取key/iv失败:', error);
}
};
接下来是AES加解密核心工具类(crypto.ts),基于 AES-CBC 模式封装通用的加密/解密方法,支持字符串、对象等多种数据类型,兼容异常处理:
// 引入crypto-js加密库,用于实现AES加密解密
import CryptoJS from 'crypto-js/crypto-js';
// 引入Token拆分工具函数,用于从Token中提取加密所需的key和iv
import { TokenSplitHandle } from './tools';
/**
* @description Crypto加密工具类 - 基于AES-CBC模式实现数据加密和解密
* @author ShiPengCheng
* @export
* @class Crypto
*/
export default class Crypto {
/**
* @description AES-CBC模式加密方法
* @static
* @param {*} word 需要加密的数据(支持字符串/对象,对象会自动转为JSON字符串)
* @return {*} 加密后的16进制字符串
* @memberof Crypto
*/
static Encrypt(word: any) {
try {
// 从Token中拆分出加密所需的key(前16位)和iv(后16位)
const { iv, key } = TokenSplitHandle();
// 边界判断:如果key/iv获取失败,直接返回(避免加密异常)
if (!iv || !key) return;
// 检测数据类型:如果是对象,先转为JSON字符串(因为加密只能处理字符串)
if (typeof word == 'object') {
word = JSON.stringify(word);
}
// 执行AES-CBC加密
let encrypted = CryptoJS.AES.encrypt(
word, // 待加密的原始数据
CryptoJS.enc.Utf8.parse(key), // 将key转为Utf8编码的字节数组(AES要求key为16/24/32位)
{
iv: CryptoJS.enc.Utf8.parse(iv), // 偏移量iv(CBC模式必须,要求16位)
mode: CryptoJS.mode.CBC, // 加密模式:CBC(需配合iv使用,比ECB更安全)
padding: CryptoJS.pad.Pkcs7, // 填充方式:Pkcs7(补齐数据长度至块大小的整数倍)
}
);
// 将加密结果转为16进制字符串返回(也可返回base64,根据后端要求调整)
return encrypted.ciphertext.toString(CryptoJS.enc.Hex);
} catch (error) {
// 捕获加密异常,避免程序崩溃
console.error('AES加密失败:', error);
}
}
/**
* @description AES-CBC模式解密方法
* @static
* @param {*} ciphertextBase64 加密后的16进制字符串
* @return {*} 解密后的原始字符串(若为对象可自行JSON.parse)
* @memberof Crypto
*/
static Decrypt(ciphertextBase64: any) {
try {
// 从Token中拆分出解密所需的key和iv(需与加密时一致)
const { iv, key } = TokenSplitHandle();
// 边界判断:如果key/iv获取失败,直接返回
if (!iv || !key) return;
// 步骤1:将16进制密文解析为CryptoJS可处理的格式
const encryptedHex = CryptoJS.enc.Hex.parse(ciphertextBase64);
// 步骤2:将16进制转为Base64格式(CryptoJS解密要求输入为Base64)
const encryptedBase64 = CryptoJS.enc.Base64.stringify(encryptedHex);
// 执行AES-CBC解密
const decrypted = CryptoJS.AES.decrypt(
encryptedBase64, // 待解密的Base64格式密文
CryptoJS.enc.Utf8.parse(key), // 解密密钥(需与加密时一致)
{
iv: CryptoJS.enc.Utf8.parse(iv), // 偏移量iv(需与加密时一致)
mode: CryptoJS.mode.CBC, // 解密模式(需与加密时一致)
padding: CryptoJS.pad.Pkcs7, // 填充方式(需与加密时一致)
}
).toString(CryptoJS.enc.Utf8); // 将解密结果转为Utf8字符串
// 返回解密后的原始数据
return decrypted;
} catch (error) {
// 捕获解密异常,便于排查问题
console.error('AES解密失败:', error);
}
}
}
下面是实战调用示例,首先是接口请求参数加密(前端→后端),在接口请求前对参数加密,将加密后的字符串传给后端:
import Crypto from '@/utils/crypto'; // 引入加密工具类
// 1. 准备接口请求参数(对象/字符串都支持)
const requestParams = {
username: 'admin',
password: '123456',
timestamp: new Date().getTime()
};
// 2. 对参数进行AES加密
const encryptedParams = Crypto.Encrypt(requestParams);
console.log('加密后的参数:', encryptedParams); // 输出16进制加密字符串
// 3. 调用接口(将加密后的参数传给后端)
const apiRequest = async () => {
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: encryptedParams // 加密后的参数
})
});
const result = await res.json();
console.log('接口响应:', result);
} catch (error) {
console.error('接口调用失败:', error);
}
};
// 执行接口调用
apiRequest();
然后是接口返回值解密(后端→前端),对接收到的加密返回值进行解密,还原原始数据:
import Crypto from '@/utils/crypto'; // 引入加密工具类
// 模拟后端返回的加密数据(实际从接口响应中获取)
const encryptedResponse = '7e8f9d0a1b2c3d4e5f6a7b8c9d0e1f2a'; // 示例16进制密文
// 对返回值进行AES解密
const decryptedResponse = Crypto.Decrypt(encryptedResponse);
console.log('解密后的原始数据:', decryptedResponse); // 输出解密后的字符串
// 若原始数据是对象,需转为JSON对象
if (decryptedResponse) {
const responseData = JSON.parse(decryptedResponse);
console.log('解析后的对象:', responseData);
}
还有日常开发中对普通字符串的加解密操作示例:
import Crypto from '@/utils/crypto';
// 加密普通字符串
const plainText = 'hello world';
const encryptedText = Crypto.Encrypt(plainText);
console.log('加密后的字符串:', encryptedText);
// 解密字符串
const decryptedText = Crypto.Decrypt(encryptedText);
console.log('解密后的字符串:', decryptedText); // 输出:hello world
最后是核心说明与避坑指南和总结,加密模式选择上,采用AES-CBC模式(需配合iv使用),相比ECB模式更安全(ECB无偏移量,相同明文加密结果相同);必须保证key/iv为16位(AES-128),若需更高安全级可使用24位(AES-192)或32位(AES-256)。数据兼容性方面,加密方法自动将对象转为JSON字符串,解密后需手动JSON.parse还原对象;加密结果默认返回16进制字符串,可根据后端要求改为Base64(修改encrypted.toString(CryptoJS.enc.Base64))。密钥安全性上,key/iv从用户SessionId拆分生成,避免硬编码(硬编码密钥易被反编译获取);需保证SessionId长度≥16位,否则拆分失败,建议后端生成SessionId时控制长度。异常处理方面,所有核心方法均包含try-catch,避免加解密失败导致程序崩溃;加密/解密前先判断key/iv是否存在,防止空值导致的加解密异常。本文实现的前端接口加解密方案具备以下特点:1. 基于AES-CBC模式,兼顾安全性与兼容性,适配绝大多数后端语言;2. 密钥动态生成(从SessionId拆分),避免硬编码风险;3. 工具类封装通用,支持字符串、对象等多种数据类型,可直接复用;4. 全程带异常处理,保证项目稳定性。该方案可直接应用于Vue3 + Pinia项目,也可适配React、Angular等其他前端框架,只需调整密钥获取逻辑即可。
(注:文档部分内容可能由 AI 生成)