通过Postman实现API网关的请求签名与调试
1. 前言
Postman是一个非常强大的HTTP发包测试工具, 目前Postman已经提供了Windows/Mac/Linux系统的客户端的下载,使用很方便。不过API网关的调试,需要对HTTP请求进行签名才能调用,无法使用简单的curl
等发包工具完成,但我们可以使用Postman工具提供的Pre-request Script脚本来实现API网关的签名功能,实现API的调试功能,本文主要介绍。
2. API网关签名算法介绍
API网关的签名机制详细可以参考官网文档,这里简要介绍一下。
API网关的签名需要通过API网关的AppKey和AppSecret进行,Key/Secret可以在API网关的控制台上获得,并确保API已经发布,并且针对特定的APP做了授权操作。
针对一个普通请求,API网关的签名过程如下
2.1. 添加以下头用于辅助签名与安全认证
- Date: 日期头
- X-Ca-Key:{AppKey}
- X-Ca-Nonce:API调用者生成的 UUID, 实现防重放功能
- Content-MD5: 当请求Body为非Form表单时,用于校验Body是否被篡改,
2.2. 组织需要签名的字符串StringToSign
{HTTPMethod} + "\n" +
{Accept} + "\n" +
{Content-MD5} + "\n"
{Content-Type} + "\n" +
{Date} + "\n" +
{SignatureHeaders} +
{UrlToSign}
- Accept、Content-MD5、Content-Type、Date 如果为空也需要添加换行符"n"
- 只有From为非表单的方式才需要计算Content-MD5,计算方法为
base64Encode(md5(body.getBytes("UTF-8"))
- SignatureHeaders: 以
{HeaderName}:{HeaderValue} + "\n"
的方式按照字符串顺序从小到大顺序添加, 建议加入签名的头为X-Ca-Key
,X-Ca-Nonce
, 其他头客户端实现可自行选择是否加入签名。 - UrlToSign: 将所有的Form字段和QueryString字段放在一起按照
Name
进行排序,如果Content-Type
不是application/x-www-form-urlencoded
类型则不拆开Form字段。将排序好的键值对加到Path
后面得到UrlToSign
, 例如请求/demo?c=1&a=2
, Form为b=3
则UrlToSign
=/demo?a=2&b=3&c=1
2.3. 计算签名并附加签名相关Headers
目前推荐使用HMacSHA256
算法计算签名,签名的计算需要appSecret,计算方法为:
signature = base64(hmacSHA256(stringToSign.getBytes("UTF-8), appSecret))
, 计算完毕后还需要添加以下Headers:
- 添加Header:
X-Ca-Siguature:{signature}
- 添加Header:
X-Ca-SignatureMethod:HmacSHA256
- 添加Header:
X-Ca-SignatureHeaders:X-Ca-Key,X-Ca-Nonce
2.4. 签名错误排查方法
- 当签名校验失败时,API网关会将服务端的
StringToSign
放到HTTP应答的Header中返回到客户端,Key为:X-Ca-Error-Message
,只需要将本地计算的StringToSign
与服务端返回的StringToSign
进行对比即可找到问题,注意服务端返回的StringToSign
将回车替换为了#
,对比是请注意; - 如果服务端与客户端的一致请检查用于签名计算的密钥是否正确
3. 使用Pre-request Script实现签名算法
根据上一节的描述,实现API网关调试的关键问题在于如何实现请求签名,Postman
提供了可以通过JavaScript进行定制的, 通过阅读Pre-request Script的开发文档, 我们可以通过Pre-request Script脚本
实现API网关的签名功能。
3.1. 使用全局变量预制签名需要添加的头
不过目前Postman
不允许直接在脚本中修改请求,所以我们只能使用预制签名头并使用全局变量赋值的方式完成签名头的添加,我们将需要签名的头都预制在Postman的请求Header中,可以通过Bulk Edit
模式实现添加,Bulk Edit
请参照下图进行切换
切换为Bulk Edit
模式后,可以将如下字符串复制粘贴到输入框当中,被{{}}
括住的就是Postman的全局变量,我们在脚本中实现替换。Form内容的可以不添加Content-MD5头
Date:{{Date}}
Content-MD5:{{Md5}}
X-Ca-Nonce:{{Nonce}}
X-Ca-Key:{{AppKey}}
X-Ca-Signature:{{Signature}}
X-Ca-SignatureMethod:HmacSHA356
X-Ca-Signature-Headers:{{SignatureHeaders}}
粘贴后效果如图
3.2. 使用Pre-request Script
脚本实现签名功能
点击红圈圈住的位置,可以输入Pre-request Script
,请复制粘贴下面提供的代码到文本框当中
var appKey = "<YOUR APP KEY>";
var appSecret = "<YOUR APP SECRET>";
var md5 = calcMd5();
var date = new Date().toString();
var nonce = createUuid();
var textToSign = "";
textToSign += request.method + "\n";
textToSign += request.headers["accept"] + "\n";
textToSign += md5 + "\n";
textToSign += request.headers["content-type"] + "\n";
textToSign += date + "\n";
var headers = headersToSign();
var signatureHeaders;
var sortedKeys = Array.from(headers.keys()).sort()
for (var headerName of sortedKeys) {
textToSign += headerName + ":" + headers.get(headerName) + "\n";
signatureHeaders = signatureHeaders ? signatureHeaders + "," + headerName : headerName;
}
textToSign += urlToSign();
console.log("textToSign\n" + textToSign.replace(/\n/g, "#"));
var hash = CryptoJS.HmacSHA256(textToSign, appSecret)
console.log("hash:" + hash)
var signature = hash.toString(CryptoJS.enc.Base64)
console.log("signature:" + signature)
pm.globals.set('AppKey', appKey);
pm.globals.set('Md5', md5);
pm.globals.set("Date", date);
pm.globals.set("Signature", signature);
pm.globals.set("SignatureHeaders", signatureHeaders);
pm.globals.set("Nonce", nonce);
function headersToSign() {
var headers = new Map();
for (var name in request.headers) {
name = name.toLowerCase();
if (!name.startsWith('x-ca-')) {
continue;
}
if (name === "x-ca-signature" || name === "x-ca-signature-headers" || name == "x-ca-key" || name === 'x-ca-nonce') {
continue;
}
var value = request.headers[name];
headers.set(name, value);
}
headers.set('x-ca-key', appKey);
headers.set('x-ca-nonce', nonce);
return headers;
}
function urlToSign() {
var params = new Map();
var contentType = request.headers["content-type"];
if (contentType && contentType.startsWith('application/x-www-form-urlencoded')) {
const formParams = request.data.split("&");
formParams.forEach((p) => {
const ss = p.split('=');
params.set(ss[0], ss[1]);
})
}
const ss = request.url.split('?');
if (ss.length > 1 && ss[1]) {
const queryParams = ss[1].split('&');
queryParams.forEach((p) => {
const ss = p.split('=');
params.set(ss[0], ss[1]);
})
}
var sortedKeys = Array.from(params.keys())
sortedKeys.sort();
var l1 = ss[0].lastIndexOf('/');
var url = ss[0].substring(l1);
var first = true;
var qs
for (var k of sortedKeys) {
var s = k + "=" + params.get(k);
qs = qs ? qs + "&" + s : s;
console.log("key=" + k + " value=" + params.get(k));
}
return qs ? url + "?" + qs : url;
}
function calcMd5() {
var contentType = request.headers["content-type"];
if (request.data && !contentType.startsWith('application/x-www-form-urlencoded')) {
var data = request.data;
var md5 = CryptoJS.MD5(data);
var md5String = md5.toString(CryptoJS.enc.Base64);
console.log("data:" + data + "\nmd5:" + md5String);
return md5String;
} else {
return "";
}
}
function createUuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
接下来我们就可以实现API网关的调试了