跳到主要内容

Apifox 参数自动签名和自动解密教程:提升API开发效率

· 阅读需 7 分钟
阿狸先森
全栈开发

ApiFox图

在 API 开发中,数据安全和接口验证是至关重要的环节。Apifox 作为一款强大的 AP I开发工具,提供前置脚本和后缀脚本的功能,通过这两个钩子就能满足自动签解的过程。本教程将详细介绍如何在 Apifox 中配置和使用这些功能。

前言: API安全自动化的重要性

现代API开发中,安全机制不再是可选项而是必备项。通过Apifox的前置脚本和后置脚本功能,我们可以轻松实现参数自动签名响应自动解密,构建完整的安全自动化流程:

为什么需要参数签名

  • 防止请求被篡改
  • 验证请求来源合法性
  • 防止重放攻击
  • 保障数据传输安全

签名规则示例

  1. 将所有参数按参数名升序排列
  2. 将参数名和参数值拼接成字符串
  3. 在字符串末尾加上API密钥
  4. 对拼接后的字符串进行哈希计算

为什么要响应参数加密

在API通信中,响应参数的加密与请求签名同等重要,它们共同构成了API安全防护的双重保障。以下是响应参数加密的核心价值:

1. 防止敏感数据泄露(核心价值)

  • 隐私保护:用户个人信息、支付数据等敏感内容若以明文传输,可能被中间人攻击截获
  • 合规要求:GDPR、PCI-DSS等法规明确要求敏感数据必须加密传输
  • 商业机密保护:防止竞争对手通过监听API获取业务核心数据

2. 防御特定网络攻击

  • 中间人攻击(MITM):加密后即使数据被截获也无法直接读取
  • 流量分析攻击:加密可隐藏数据特征,防止攻击者通过模式分析推断业务逻辑
  • 数据篡改攻击:配合签名机制可识别加密数据的非法修改

3. 保障数据完整性

  • 防篡改校验:加密通常与MAC(消息认证码)结合使用,确保数据在传输过程中未被修改
  • 版本控制:通过加密密钥版本管理,可强制客户端升级到安全版本

4. 特殊业务场景需求

  • 金融级安全:支付金额、账户余额等财务数据必须加密
  • 医疗健康数据:HIPAA等法规对医疗信息有严格加密要求
  • 物联网通信:防止设备数据被伪造或重放

加密 vs 签名的区别:

特性参数签名响应加密
主要目的验证请求合法性保护数据机密性
数据可见性明文传输密文传输
处理位置客户端生成/服务端验证服务端加密/客户端解密
典型算法HMAC-SHA256, MD5AES-256, RSA-OAEP

常见的解密机制

配置项说明示例值注意事项
解密密钥用于解密的密钥字符串your_decryption_key_123建议使用环境变量存储(如{{DECRYPT_KEY}}
解密算法选择对称/非对称加密算法AES-256-CBC RSA-OAEP需与服务端保持一致 AES适合大数据量,RSA适合小数据+密钥交换
加密模式分组密码的工作模式(仅对称加密需要)CBC ECB GCMECB模式不安全,推荐使用CBC或GCM模式
填充方式数据块填充方案(仅对称加密需要)PKCS5Padding PKCS7必须与服务端一致,否则解密失败
IV向量初始化向量(CBC/GCM等模式需要)initial_vector_456长度需符合算法要求(如AES-CBC需16字节)
响应数据位置加密数据在响应体中的JSON路径response.data result.payload支持多级路径,空值表示整个响应体都是加密数据

Apifox 如何自动签名

签名流程详解

环境变量配置

  1. 在Apifox环境变量中添加APP_KEY存储签名密钥

在Apifox中设置环境变量

编写一个公共脚本

项目设置 -> 公共脚本

公共脚本入口

新建脚本

脚本逻辑

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 的逻辑,还是得结合自己业务区实现,大概逻辑是这样。我区分常见的请求方法的带参逻辑

如何使用签名脚本

在接口请求界面中 ,选择前置操作 -> 公共脚本 -> 参数签名

image-20250620105702914

image-20250620105854192

签名效果验证

发送GET请求后,URL自动添加签名参数:数

GET http://localhost:7001/api?sign=2694b19b16a333a5d059c1c29ae0afeb24b2205b291bae7799097557995e50c2

image-20250620110240637

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));

如何使用解密脚本

image-20250620112136910

解密效果对比

加密响应

{
"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": ""
}
}

全局自动化配置技巧

目录级自动化设置

在项目目录上应用脚本,实现子接口自动继承,就不用每次使用时都设置一次脚本。

image-20250620114016919

Token自动管理

通过后置脚本自动提取token并存储到环境变量:

// 在解密脚本中添加
const token = response.data?.token;
if(token) {
pm.environment.set('token', token);
console.log("Token已更新:", token);
}

同理全局使用 token 也是这样 (鉴权方式根据实际的来)

image-20250620114121632

实践建议

密钥安全

  • 永远不要在脚本中硬编码密钥
  • 使用{{VAR}}语法引用环境变量
  • 定期轮换密钥

总体流程

请求签名自动化流程

响应解密自动化流程

结语

  • 通过Apifox的脚本功能实现参数自动签名和响应自动解密,您可以:

  • ✅ 减少手动操作错误

  • ✅ 提升API测试效率50%以上

  • ✅ 确保敏感数据全程加密=

  • ✅ 实现团队协作标准化

  • ⚠️如果不用接口工具,使用类似 jest 的工具测试的话,具体思路也是差多的哈