天天看點

通過Postman實作API網關的請求簽名與調試

1. 前言

Postman

是一個非常強大的HTTP發包測試工具, 目前Postman已經提供了Windows/Mac/Linux系統的用戶端的下載下傳,使用很友善。不過API網關的調試,需要對HTTP請求進行簽名才能調用,無法使用簡單的

curl

等發包工具完成,但我們可以使用Postman工具提供的

Pre-request Script腳本

來實作API網關的簽名功能,實作API的調試功能,本文主要介紹如何使用Postman調試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}

  • X-Ca-SignatureMethod:HmacSHA256

  • 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

, 我們可以通過編寫簽名腳本實作API網關的簽名功能,詳見

Pre-request Script的開發文檔

, 。

3.1. 使用全局變量預制簽名需要添加的頭

不過目前

Postman

不允許直接在腳本中修改請求,是以我們隻能使用預制簽名頭并使用全局變量指派的方式完成簽名頭的添加,我們将需要簽名的頭都預制在Postman的請求Header中,可以通過

Bulk Edit

模式實作添加,

Bulk Edit

請參照下圖進行切換

通過Postman實作API網關的請求簽名與調試

切換為

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}}           

粘貼後效果如圖

通過Postman實作API網關的請求簽名與調試

3.2. 使用

Pre-request Script

腳本實作簽名功能

通過Postman實作API網關的請求簽名與調試

點選紅圈圈住的位置,可以輸入

Pre-request Script

,請複制粘貼下面提供的

Java Script

代碼到文本框當中 ,将AppKey和AppSecret替換為自己的

Key/Secret

即可實作調試功能

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網關的調試了