Apifox 参数自动签名和自动解密教程:提升API开发效率
· 阅读需 7 分钟
在 API 开发中,数据安全和接口验证是至关重要的环节。Apifox 作为一款强大的 AP I开发工具,提供前置脚本和后缀脚本的功能,通过这两个钩子就能满足自动签解的过程。本教程将详细介绍如何在 Apifox 中配置和使用这些功能。
前言: API安全自动化的重要性
现代API开发中,安全机制不再是可选项而是必备项。通过Apifox的前置脚本和后置脚本功能,我们可以轻松实现参数自动签名和响应自动解密,构建完整的安全自动化流程:
为什么需要参数签名
- 防止请求被篡改
- 验证请求来源合法性
- 防止重放攻击
- 保障数据传输安全
签名规则示例
- 将所有参数按参数名升序排列
- 将参数名和参数值拼接成字符串
- 在字符串末尾加上API密钥
- 对拼接后的字符串进行哈希计算
为什么要响应参数加密
在API通信中,响应参数的加密与请求签名同等重要,它们共同构成了API安全防护的双重保障。以下是响应参数加密的核心价值:
1. 防止敏感数据泄露(核心价值)
- 隐私保护:用户个人信息、支付数据等敏感内容若以明文传输,可能被中间人攻击截获
- 合规要求:GDPR、PCI-DSS等法规明确要求敏感数据必须加密传输
- 商业机密保护:防止竞争对手通过监听API获取业务核心数据
2. 防御特定网络攻击
- 中间人攻击(MITM):加密后即使数据被截获也无法直接读取
- 流量分析攻击:加密可隐藏数据特征,防止攻击者通过模式分析推断业务逻辑
- 数据篡改攻击:配合签名机制可识别加密数据的非法修改
3. 保障数据完整性
- 防篡改校验:加密通常与MAC(消息认证码)结合使用,确保数据在传输过程中未被修改
- 版本控制:通过加密密钥版本管理,可强制客户端升级到安全版本
4. 特殊业务场景需求
- 金融级安全:支付金额、账户余额等财务数据必须加密
- 医疗健康数据:HIPAA等法规对医疗信息有严格加密要求
- 物联网通信:防止设备数据被伪造或重放
加密 vs 签名的区别:
特性 | 参数签名 | 响应加密 |
---|---|---|
主要目的 | 验证请求合法性 | 保护数据机密性 |
数据可见性 | 明文传输 | 密文传输 |
处理位置 | 客户端生成/服务端验证 | 服务端加密/客户端解密 |
典型算法 | HMAC-SHA256, MD5 | AES-256, RSA-OAEP |
常见的解密机制
配置项 | 说明 | 示例值 | 注意事项 |
---|---|---|---|
解密密钥 | 用于解密的密钥字符串 | your_decryption_key_123 | 建议使用环境变量存储(如{{DECRYPT_KEY}} ) |
解密算法 | 选择对称/非对称加密算法 | AES-256-CBC RSA-OAEP | 需与服务端保持一致 AES适合大数据量,RSA适合小数据+密钥交换 |
加密模式 | 分组密码的工作模式(仅对称加密需要) | CBC ECB GCM | ECB模式不安全,推荐使用CBC或GCM模式 |
填充方式 | 数据块填充方案(仅对称加密需要) | PKCS5Padding PKCS7 | 必须与服务端一致,否则解密失败 |
IV向量 | 初始化向量(CBC/GCM等模式需要) | initial_vector_456 | 长度需符合算法要求(如AES-CBC需16字节) |
响应数据位置 | 加密数据在响应体中的JSON路径 | response.data result.payload | 支持多级路径,空值表示整个响应体都是加密数据 |
Apifox 如何自动签名
签名流程详解
环境变量配置
- 在Apifox环境变量中添加
APP_KEY
存储签名密钥
编写一个公共脚本
项目设置 -> 公共脚本
脚本逻辑
const CryptoJS = require("crypto-js");
const key = pm.environment.get("APP_KEY"); // 这里是我们第一步填写的 key 名,注意签名算法前后端一致
let param = {};
pm.request.url.query.each(item => {
param[item.key] = item.value;
console.log('url的参数', param)
});
if (pm.request.body) {
let bodyData;
switch (pm.request.body.mode) {
case 'formdata':
bodyData = pm.request.body.formdata;
break;
case 'urlencoded':
bodyData = pm.request.body.urlencoded;
break;
case 'raw': {
const contentType = pm.request.headers.get('content-type');
if (contentType && contentType.toLowerCase().includes('application/json')) {
try {
const jsonData = JSON.parse(pm.request.body.raw);
for (let k in jsonData) {
if (jsonData[k] !== undefined && jsonData[k] !== null && jsonData[k] !== '') {
param[k] = jsonData[k];
}
}
} catch (e) {
console.log("请求 body 不是 JSON 格式");
}
}
break;
}
default:
break;
}
if (bodyData) {
bodyData.each(item => {
if (!item.disabled && item.value !== '') {
param[item.key] = item.value;
}
});
}
}
const filteredKeys = Object.keys(param).filter(k => k !== "sign");
filteredKeys.sort();
const pairs = [];
for (const k of filteredKeys) {
const val = param[k];
if (val === null || val === undefined || val === '') {
continue;
}
if (typeof val === 'object') {
const str = JSON.stringify(val);
const hashedValue = CryptoJS.MD5(str).toString(CryptoJS.enc.Hex);
pairs.push(`${k}=${hashedValue}`);
} else {
pairs.push(`${k}=${val}`);
}
}
const strToSign = pairs.join("&");
const sign = CryptoJS.HmacSHA256(strToSign, key);
const signature = sign.toString(CryptoJS.enc.Hex);
const method = pm.request.method.toUpperCase();
if (method === 'GET' || method === 'DELETE') {
pm.request.url.query.upsert({ key: "sign", value: signature });
}
else if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
if (pm.request.body) {
switch (pm.request.body.mode) {
case 'urlencoded':
pm.request.body.urlencoded.upsert({ key: "sign", value: signature });
break;
case 'formdata':
pm.request.body.formdata.upsert({ key: "sign", value: signature });
break;
case 'raw':
try {
const contentType = pm.request.headers.get('content-type');
if (contentType && contentType.toLowerCase().includes('application/json')) {
const body = JSON.parse(pm.request.body.raw || '{}');
body.sign = signature;
pm.request.body.update({
mode: 'raw',
raw: JSON.stringify(body)
});
} else {
pm.request.headers.upsert({
key: 'Content-Type',
value: 'application/json'
});
pm.request.body.update({
mode: 'raw',
raw: JSON.stringify({ sign: signature })
});
}
} catch (e) {
console.error("更新 JSON Body 失败:", e);
pm.request.headers.upsert({
key: 'Content-Type',
value: 'application/json'
});
pm.request.body.update({
mode: 'raw',
raw: JSON.stringify({ sign: signature })
});
}
break;
default:
pm.request.headers.upsert({
key: 'Content-Type',
value: 'application/json'
});
pm.request.body.update({
mode: 'raw',
raw: JSON.stringify({ sign: signature })
});
}
} else {
pm.request.headers.upsert({
key: 'Content-Type',
value: 'application/json'
});
pm.request.body.update({
mode: 'raw',
raw: JSON.stringify({ sign: signature })
});
}
}
console.log("待签名字符串:", strToSign);
console.log("生成签名:", signature);
这个请求自动携带 sign 的逻辑,还是得结合自己业务区实现,大概逻辑是这样。我区分常见的请求方法的带参逻辑
如何使用签名脚本
在接口请求界面中 ,选择前置操作 -> 公共脚本 -> 参数签名
签名效果验证
发送GET请求后,URL自动添加签名参数:数
GET http://localhost:7001/api?sign=2694b19b16a333a5d059c1c29ae0afeb24b2205b291bae7799097557995e50c2
Apifox 如果自动解密响应
在做后端公共响应封装时,我们可以加多一个字段isEncrypt
区分一下是不是加密的,省了前端每次都得走解密逻辑。
解密流程解析
环境变量准备
创建解密所需环境变量:
PWD_KEY
:AES解密密钥DEFAULT_IV
:初始化向量
接下来我们还是新建一个公共脚本,名字为解密回参。
本次采取 AES-CBC 模式解密模式(前后端一定要统一)
脚本逻辑
const CryptoJS = require("crypto-js");
const originalResponse = pm.response.json();
const response = JSON.parse(JSON.stringify(originalResponse));
if (response.data && response.isEncrypt === true) {
const PWD_KEY = pm.environment.get("PWD_KEY");
const DEFAULT_IV = pm.environment.get("DEFAULT_IV");
if (!PWD_KEY || !DEFAULT_IV) {
console.error("缺少必要的环境变量:PWD_KEY 或 DEFAULT_IV");
return;
}
const key = CryptoJS.enc.Utf8.parse(PWD_KEY);
const iv = CryptoJS.enc.Utf8.parse(DEFAULT_IV);
try {
const encryptedBytes = CryptoJS.enc.Base64.parse(response.data);
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: encryptedBytes },
key,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
const decryptedUtf8 = decrypted.toString(CryptoJS.enc.Utf8);
if (!decryptedUtf8) {
throw new Error("解密结果为空");
}
try {
response.data = JSON.parse(decryptedUtf8);
console.log("解密成功,数据已转换为JSON对象");
} catch (e) {
response.data = decryptedUtf8;
console.log("解密成功,数据为非JSON格式");
}
pm.response.setBody(response)
const token = response.data?.token; // 这个是一个细节,如果含有 token ,我们就把 token 写入环境变量。便于我们后续使用
if(token) {
console.log("写入token:", { token });
pm.environment.set('token', token);
}
console.log("修改后的响应:", response);
} catch (error) {
console.error("解密错误:", error);
}
}
pm.visualizer.set(JSON.stringify(originalResponse, null, 2));
如何使用解密脚本
解密效果对比
加密响应
{
"message": "ok",
"isEncrypt": true,
"statusCode": 200,
"data": "RYDH2bQfVnvHvQp3pYok74ted2HMCrU+jN7VYbX0ALJnIoPDgWcIiQg048d2OCtyeGhT6jm3yyPIYNlZfiuHxW+BiI41/1phKj4ruXLJRQGX2lgU/3TCvywNKvXVZ6bGRx+DqM6WgIZsAqCK68oNRw9aUWKtTdtrb2uX0SJtSXqrkUvYi5smEq8PHAGSYo4svokhGpspQdTzirq2RPdb2hSc6KAsczPtviwEQZsGPS4sHSrXIGcI/w8M0dMhQpobt5mukaJ2bLJH0wM4CWy+/horSqwqlSJW0LuDPoTT/YTDYkFhvPewjzJQVKnHkea90mvZPJbap2VWOCwYL9vgh0TA6TUDoGb7U7UjMRft0RUrQhO198zRO0Cg+gCxL7/q"
}
解密后响应
{
"message": "ok",
"isEncrypt": true,
"statusCode": 200,
"data": {
"id": "546275570",
"nickname": ""
}
}
全局自动化配置技巧
目录级自动化设置
在项目目录上应用脚本,实现子接口自动继承,就不用每次使用时都设置一次脚本。
Token自动管理
通过后置脚本自动提取token并存储到环境变量:
// 在解密脚本中添加
const token = response.data?.token;
if(token) {
pm.environment.set('token', token);
console.log("Token已更新:", token);
}
同理全局使用 token 也是这样 (鉴权方式根据实际的来)
实践建议
密钥安全:
- 永远不要在脚本中硬编码密钥
- 使用
{{VAR}}
语法引用环境变量 - 定期轮换密钥
总体流程
请求签名自动化流程
响应解密自动化流程
结语
-
通过Apifox的脚本功能实现参数自动签名和响应自动解密,您可以:
-
✅ 减少手动操作错误
-
✅ 提升API测试效率50%以上
-
✅ 确保敏感数据全程加密=
-
✅ 实现团队协作标准化
-
⚠️如果不用接口工具,使用类似
jest
的工具测试的话,具体思路也是差多的哈