md5

背景​

当前平台服务端接入的鉴权方式是【鉴权指南 md5 - v1.0】,由于该签名方式并未使用通用的方式来处理一些复杂的参数类型(如:list、object 等),而是依赖了语言库的一些特性来实现,也就是说,不同语言库的实现方式可能存在差异,导致在复杂参数类型上并不适用。为了解决这个问题,平台对此做出了优化,引入了【鉴权指南 md5 - v1.1】。

与v1.0版本的签名方式相比,有什么不同​

对于参数的 value 值转换做了如下定义:

类型 v1.0定义 v1.1定义
string 直接拼接字符串内容即可,没有引号,如:abc 与v1.0相同
number 需要转成字符串后拼接,如:1231.03 与v1.0相同
bool 需要转成 true/false 后拼接 与v1.0相同
空(null) 需要转成空字符串("")后拼接 与v1.0相同
list 一般会转为 string 的方式来使用,不会直接使用 list 结构本身 与v1.0不同,需要转成 json 类型的 array 后拼接,如: [1, 2, 3]
object 一般会转为 string 的方式来使用,不会直接使用 object 结构本身 与v1.0不同,需要转成 json 类型的 object 后拼接,如:{"age":20,"height":170.3,"name":"张三"}

对于复杂结构(list、object),内嵌的各种类型都以 json 为标准,针对v1.1的list的类型举例:

类型 例子
number list [1,2,3]
string list ["a","c"]
object list [{"age":20,"name":"张三"},{"age":28,"name":"李四"}]

注意:复杂结构的 JSON 串不需要对内部字段进行拆分和排序,但要保证签名时对复杂结构的 JSON 序列化方式要和 HTTP 发起 JSON 请求的序列化方式一致。

使用方式

签名算法

MD5

待签名参数

获取所有请求参数(防止新增参数导致签名失败),除去 sign,其他请求参数(包含空值的参数,即 key="" )都要参加验签。

详细步骤

STEP1:获取签名密钥
STEP2:参数拼接,获取 $source 字符串
  1. 对所有待签名参数按照字段名的ASCII码从小到大排序;
  2. 使用键值对的格式( key1=value1&key2=value2 )连接参数,拼接成字符串;
  3. 对拼接完参数的字符串进行 urlencode 编码,获得 $source 字符串;
STEP3:利用密钥生成请求参数的签名摘要
  1. 将上述 $source 字符串与秘钥 AppSecret(签名密钥) 通过 & 符号进行拼接,即:$source&AppSecret(签名密钥)
  2. 将拼接后的字符串进行MD5哈希签名得到最终 sign 值(长度为32);
  3. sign 值作为请求参数的一部分,发送到服务端进行鉴权。

使用举例

假设 AppSecret(签名密钥)38f9c7af24ff11edb92900163e30ef81,请求参数为:

  • b = 1
  • a = "飞鱼"
  • d = 0.1
  • c = null
  • e = [1,2,3]
  • f = {"g":"h","i":1}
  • x = true
  • y = false

实际计算步骤如下:

  1. 通过步骤2的排序和连接,可以得到:a=飞鱼&b=1&c=&d=0.1&e=[1,2,3]&f={"g":"h","i":1}&x=true&y=false
  2. 通过步骤2的 urlencode 编码,可以得到 a%3D%E9%A3%9E%E9%B1%BC%26b%3D1%26c%3D%26d%3D0.1%26e%3D%5B1%2C2%2C3%5D%26f%3D%7B%22g%22%3A%22h%22%2C%22i%22%3A1%7D%26x%3Dtrue%26y%3Dfalse
  3. 通过步骤3的尾部追加 App Secret(签名密钥),可以得到:a%3D%E9%A3%9E%E9%B1%BC%26b%3D1%26c%3D%26d%3D0.1%26e%3D%5B1%2C2%2C3%5D%26f%3D%7B%22g%22%3A%22h%22%2C%22i%22%3A1%7D%26x%3Dtrue%26y%3Dfalse&38f9c7af24ff11edb92900163e30ef81
  4. 计算 MD5 哈希值,得到 sign 值:c30223cb4b65b611300ffc15c8d7babb

签名样例

Golang
package main

import (
    "crypto/md5"
    "encoding/hex"
    "encoding/json"
    "net/url"
    "sort"
    "strings"
)

func Sign(params map[string]interface{}, appSecret string) string {
    keys := make([]string, 0, len(params))
    for k := range params {
        keys = append(keys, k)
    }

    // 对key进行排序
    sort.Strings(keys)

    var kvPairs []string
    for _, k := range keys {
        if k == "sign" {
            continue
        }
        v := params[k]
        strValue := ""
        if v != nil {
            switch v := v.(type) {
            case string: // 单独处理字符串类型,防止被双引号包裹
                strValue = v
            default: // 通过 json 序列化处理其他的类型值
                jsonBytes, _ := json.Marshal(v)
                strValue = string(jsonBytes)
            }
        }
        kvPairs = append(kvPairs, k+"="+strValue)
    }

    // 拼接参数
    source := strings.Join(kvPairs, "&")
    // 进行urlencode编码
    source = strings.Replace(url.QueryEscape(source), "+", "%20", -1)

    // 拼接AppSecret
    source += "&" + appSecret

    // 生成MD5签名
    hash := md5.Sum([]byte(source))
    sign := hex.EncodeToString(hash[:])

    return sign
}
PHP
function sign($params, $appSecret)
{
    // 1、按照健值从小到大排序
    ksort($params);

    // 2、键值对按照'='拼接,排除sign参数
    $signParams = array();
    foreach ($params as $key => $val) {
        if ('sign' == $key) {
            continue;
        }
        switch (gettype($val)) {
            case 'NULL':
                $val = '';
                break;
            case 'boolean':
                $val = $val === true ? 'true' : 'false';
                break;
            case 'array':
                $val = json_encode($val, JSON_UNESCAPED_UNICODE);
                break;
            default:
                $val = (string) $val;
                break;
        }
        array_push($signParams, $key . '=' . $val);
    }

    // 3、将上述数据按照&拼接
    $signStr = join('&', $signParams);

    // 4、将拼接结果用rawurlencode编码
    $source = rawurlencode($signStr);

    // 5、md5 hash计算签名
    return md5($source . '&' . $appSecret);
}