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 网关的签名功能.
注意: 本节的代码请使用 POSTMAN Version 7.2.0 以上版本
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:HmacSHA256
- X-Ca-Signature-Headers:{
- {
- SignatureHeaders
- }
- }
粘贴后效果如图
3.2. 使用 Pre-request Script 脚本实现签名功能
点击红圈圈住的位置, 可以输入 Pre-request Script, 请复制粘贴下面提供的代码到文本框当中
注意: 本节的代码请使用 POSTMAN Version 7.2.0 以上版本
- var appKey = "YOUR APPKEY";
- var appSecret = "YOUR APPCODE";
- var md5 = calcMd5();
- var dateObject = Date;
- var date = dateObject.toLocaleString();
- var nonce = createUuid();
- var textToSign = "";
- var accept = "*/*";
- var contentType = "";
- console.log("request" + JSON.stringify(request));
- if(request.headers["accept"]){
- accept = request.headers["accept"];
- }
- if(request.headers["content-type"]){
- contentType = request.headers["content-type"];
- }
- textToSign += request.method + "\n";
- textToSign += accept + "\n";
- textToSign += md5 + "\n";
- textToSign += contentType + "\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')) {
- for(x in request.data){
- params.set(x, request.data[x]);
- }
- }
- var queryParam = pm.request.url.query.members;
- console.log("request.url" + JSON.stringify(pm.request.url))
- for (let i in queryParam) {
- params.set(queryParam[i].key, queryParam[i].value);
- }
- var sortedKeys = Array.from(params.keys())
- sortedKeys.sort();
- var url = "";
- for(var k of pm.request.url.path){
- url = url + "/" + k;
- }
- 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 = String(request.headers["content-type"]);
- console.log("data" + JSON.stringify(request.data));
- if (!JSON.stringify(request.data).startsWith('{}') && !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 网关的调试了.
注意本节代码仅供用户参考. 目前有用户反馈 POSTMAN 在 Windows 平台运行本脚本时会出现 "Can not get any response" 的情况, 建议用户尽量在 Mac 环境下使用 POSTMAN 进行调试.
来源: https://yq.aliyun.com/articles/753758