簽名驗證,GO&PHP解析json不一致方案,php多維陣列遞迴排序

aen233發表於2021-02-20

php轉go程式設計師一枚,最近公司專案也在逐漸轉go。
對接的第一步就是簽名驗證中介軟體。
需要php和go都實現同一套簽名驗證。

簽名步驟

介面資料統一採用 簽名認證。步驟如下:

  1. 約定好appKey、appSecret。
  2. 通過簽名規則生成簽名sign;
  3. 每次請求業務介面 Header 中需攜帶appKey、timestamp、 sign 。

    appSecret 傳送請求不傳,僅作為簽名加密使用

簽名規則

引數名ASCII碼從小到大排序(字典序);
假設引數:

{
    "event": "add",
    "event_desc":"add",
    "source":"crs",
    "data": {
        "order_id":1,
        "enterprise_id":1,
        "order_no":"1111"
    }
}
  1. 按照引數名ASCII字典序升序(引數如果是陣列,陣列也應ASCII字典序升序),將引數轉為json,得到引數串
    {"data":{"enterprise_id":1,"order_id":1,"order_no":"1111"},"event":"add","event_desc":"add","source":"crs"}
  2. 引數串首尾拼接appSecret並進行md5加密

遇到的問題

  1. 排序。
    go map內部儲存是有序的,只是在遍歷的時候,會隨機選一個bucket 進行。
    php 陣列順序就是接收順序。
    所以這裡是對php陣列進行了排序。網上搜了很多例子,都是隻進行了一次ksort,但是go的排序是每一層都是順序的。
    php 多維陣列遞迴排序如下:

    //多維陣列內排序
    $sortFunc = static function (array $params, callable $sortFunc): array {
     array_walk($params, static function(&$item) use ($sortFunc) {
         $item = is_array($item) ? $sortFunc($item, $sortFunc) : $item;
     });
     ksort($params);
     return $params;
    };
    $sortedArr = $sortFunc($params, $sortFunc);
  2. go和php url編碼結果不一致。
    最初的打算是使用http_build_query生成 URL-encode的字串再md5,
    go程式碼如下:

     func HttpBuildQuery(queryData url.Values) string {
         return queryData.Encode()
     }

    但是兩者生成的url編碼結果差異挺大,遂放棄。轉而使用json字串。

  3. go和php json 編碼結果不一樣。
    go語言中json.Marshal生成json時特殊html字元(常見的有&、<、>)會被轉義。

    第一次嘗試: go 設定 SetEscapeHTML(false) (放棄)

     type Test struct {
          Content   string
     }
     func sign() {
     t := new(Test)
     t.Content = "http://www.baidu.com?id=123&test=1"
     bf := bytes.NewBuffer([]byte{})
     jsonEncoder := json.NewEncoder(bf)
     jsonEncoder.SetEscapeHTML(false)
     jsonEncoder.Encode(t)
     fmt.Println(bf.String())
     }

    SetEscapeHTML(false)那個會在json最後自動拼個換行符,處理起來挺麻煩。
    第二次嘗試:go 字串替換(可行)

     inputJsonStr = strings.Replace(inputJsonStr, "\\u003c", "<", -1)
     inputJsonStr = strings.Replace(inputJsonStr, "\\u003e", ">", -1)
     inputJsonStr = strings.Replace(inputJsonStr, "\\u0026", "&", -1)

    第三次嘗試:php json_encode(增加引數JSON_HEX_AMP 和JSON_HEX_TAG,最終使用)

     $jsonStr = json_encode($sortedData, JSON_HEX_AMP + JSON_HEX_TAG + JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES);

    這裡用到的json_encode的4個引數說明:
    JSON_HEX_AMP (integer) 所有的 & 轉換成 \u0026。 自 PHP 5.3.0 起生效。
    JSON_HEX_TAG (integer) 所有的 < 和 > 轉換成 \u003C 和 \u003E。 自 PHP 5.3.0 起生效。
    JSON_UNESCAPED_SLASHES (integer) 不要編碼 /。 自 PHP 5.4.0 起生效。JSON_UNESCAPED_UNICODE (integer) 以字面編碼多位元組 Unicode 字元(預設是編碼成 \uXXXX)。 自 PHP 5.4.0 起生效。

    如果json資料中沒有特殊字元,也需要JSON_UNESCAPED_SLASHES 和 JSON_UNESCAPED_UNICODE 這兩個引數,否則php json編碼出來的結果滿篇都是 / 和 \uXXXX。

    但是非常離譜的,走一遍對比之後,結果仍然不一致。

    簽名驗證,GO&PHP解析json不一致,php多維陣列遞迴排序

    php json_encode增加引數JSON_HEX_TAG後,
    php 將 <、> 轉成了 \u003C、\u003E,大寫的,
    go 將 <、> 轉成了 \u003c、\u003e,小寫的…

    這。。。。。只能字串替換了,go的專案已經在用了,我們選擇在php程式碼中替換

    $jsonStr = str_replace("\u003C", "\u003c", $jsonStr);
    $jsonStr = str_replace("\u003E", "\u003e", $jsonStr);

最後的簽名程式碼

go

import (
    "crypto/md5"
    "encoding/hex"
    "encoding/json"
)

var appSecret = "123456"

// 驗證簽名
func verifySign() error {
    params := make(map[string]interface{})
    params["name"] = "test"
    params["domain"] = "https://www.baidu.com?name=1&id=1"
    params["ssss"] = "sssss"
    params["aaaa"] = "aaaaa"

    paramsJson, _ := json.Marshal(params)

    s := md5.New()
    s.Write([]byte(AppSecret + string(paramsJson) + AppSecret))
    sign := hex.EncodeToString(s.Sum(nil))
}

php

$appSecret = '123456';
$params = [
    'name' => 'test',
    'domain' => 'https://www.baidu.com?name=1&id=1',
];
//多維陣列內排序
$sortFunc = static function (array $params, callable $sortFunc): array {
    array_walk($params, static function(&$item) use ($sortFunc) {
        $item = is_array($item) ? $sortFunc($item, $sortFunc) : $item;
    });
    ksort($params);
    return $params;
};
$jsonStr = json_encode($sortFunc($params, $sortFunc), JSON_HEX_AMP + JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES);

$jsonStr = str_replace("\u003C", "\u003c", $jsonStr);
$jsonStr = str_replace("\u003E", "\u003e", $jsonStr);

$sign = md5($appSecret . $jsonStr . $appSecret);

工具推薦

  1. php2go
  2. json2go

其他

其他json_encode 第二個引數說明:
JSON_HEX_APOS (integer) 所有的 ‘ 轉換成 \u0027。 自 PHP 5.3.0 起生效。
JSON_HEX_QUOT (integer) 所有的 “ 轉換成 \u0022。 自 PHP 5.3.0 起生效。
JSON_FORCE_OBJECT (integer) 使一個非關聯陣列輸出一個類(Object)而非陣列。 在陣列為空而接受者需要一個類(Object)的時候尤其有用。 自 PHP 5.3.0 起生效。
JSON_NUMERIC_CHECK (integer) 將所有數字字串編碼成數字(numbers)。 自 PHP 5.3.3 起生效。
JSON_BIGINT_AS_STRING (integer) 將大數字編碼成原始字元原來的值。 自 PHP 5.4.0 起生效。
JSON_PRETTY_PRINT (integer) 用空白字元格式化返回的資料。 自 PHP 5.4.0 起生效。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章