签名验证,GO&PHP解析json不一致方案,php多维数组递归排序

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 协议》,转载必须注明作者和本文链接
讨论数量: 1

为什么不直接接收post的json参数作为字符串? 难道你是php请求go服务,go服务struct接收了json参数后,又json.marshal一下再md5算法,直接接收json参数字符串然后md5不就得了

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
php @ abc
文章
20
粉丝
94
喜欢
197
收藏
231
排名:107
访问:8.9 万
私信
所有博文
社区赞助商