# 服务端 API 预分配密钥签名指南

# 概述

访问微信游戏提供的后台 API 时,后台 API 需要认证开发者的身份。在此介绍一种基于预分配密钥对请求的内容进行签名的算法。

目前支持的算法

  • WXGAME-TOKEN-HMAC-SHA256 使用 HMAC-SHA256 进行签名,密钥为预分配密钥,内容为下述数据内容

接入前开发者需要向平台申请

  • sign_appname: 业务代号,用于唯一标识开发者
  • sign_token: 业务所对应的预分配密钥

# 签名参数

签名参数由 HTTP 请求时的以下几个 Header 指定

参数 必选 含义
X-WXGAME-SIGN-APPNAME 业务代号,在申请接入时由平台分配 $sign_appname
X-WXGAME-SIGN-METHOD 签名算法,如 WXGAME-TOKEN-HMAC-SHA256
X-WXGAME-SIGN-NONCE 随机串,每次请求均不相同,用于防重放
X-WXGAME-SIGN-TIMESTAMP 签名时间戳,不可与实际请求时间差距过大
X-WXGAME-SIGN-SIGNEDHEADERS 指定还有哪些 HTTP Header 需要参与签名,Header Key 之间使用 ; 分隔
X-WXGAME-SIGN 签名,HEX 小写

# 签名计算方法

# 1. 计算 $QUERY_PARAMS

  1. 将 QueryString 中所有的 Key 取出来按字典序排序
  2. 按 Key 排序后的顺序,从 QueryString 取出对应的 Value ,拼成 UrlEncode(key)=UrlEncode(value) 键值对,其中 UrlEncodeencodeURIComponent 规范实现
  3. 把上述所有键值对按顺序用 "&" 连接起来
$QUERY_PARAMS = UrlEncode($key1) + "=" + UrlEncode($value1)
                + "&" + UrlEncode($key2) + "=" + UrlEncode($value2)
                + ...

# 2. 计算 $HEADER_PARAMS

  1. 将上述除了 X-WXGAME-SIGN 外的认证用 Header 与 X-WXGAME-SIGN-SIGNEDHEADERS 指明的 Header Key 共同放在一个列表中 注:在 HTTP 请求中包含的 Header 才需要参与计算签名
  2. 把列表中所有的 Header Key 转为小写,并按字典序排序
  3. 按小写 Key 排序后的顺序,从 Header 中取出对应的 Value ,拼成 UrlEncode(key_lowercase)=UrlEncode(value) 键值对,其中 UrlEncodeencodeURIComponent 规范实现
  4. 把上述所有键值对按顺序用 "&" 连接起来,其中 Key 用的是小写后的 Header Key
$HEADER_PARAMS = UrlEncode($key1_lowercase) + "=" + UrlEncode(&value1)
                + "&" + UrlEncode($key2_lowercase) + "=" + UrlEncode($value2)
                + ...

# 3. 计算 $STRING_TO_SIGN

  1. 获得 HTTP 请求的方法名 $HTTP_METHOD,如 GET, POST
  2. 获得 HTTP 请求的 URI $HTTP_URI,如 /cgi-bin/some/api
  3. 获得 HTTP 请求的 Body $HTTP_BODY,若无则为空
$STRING_TO_SIGN = $HTTP_METHOD + "\n"
                  + $HTTP_URI + "\n"
                  + $QUERY_PARAMS + "\n"
                  + $HEADER_PARAMS + "\n"
                  + $HTTP_BODY

# 4. 计算签名 $SIGN

  1. 当前支持的算法是 WXGAME-TOKEN-HMAC-SHA256 ,签名时使用 HMAC-SHA256 算法
  2. $SIGN = HMAC-SHA256(key = $sign_token, value = $STRING_TO_SIGN)
  3. 其中使用的 $sign_token 为预分配密钥,每个 sign_appname 对应一个密钥

# 签名示例

curl -XPOST "https://game.weixin.qq.com/cgi-bin/comm/checksignature?param1=value1&param2=value2" \
     -H 'X-WXGAME-SIGN-APPNAME: test_appname' \
     -H 'X-WXGAME-SIGN-METHOD: WXGAME-TOKEN-HMAC-SHA256' \
     -H 'X-WXGAME-SIGN-NONCE: BEBbaQtq' \
     -H 'X-WXGAME-SIGN-TIMESTAMP: 1713172261' \
     -H 'X-WXGAME-SIGN-SIGNEDHEADERS: X-Customized-Header;User-Agent' \
     -H 'X-WXGAME-SIGN: 0f2dbfc9c7a7abd845fc08e800e560bd0a1d901b5c3eb4a84af7c1b239f93874' \
     -H 'X-Customized-Header: Customized-Value' \
     -H 'User-Agent: Random UA' \
     -d '{}'

此请求的业务代号是 test_appname ,它的预分配密钥是 O9ogYc5Dir40e4VyDAdIeTcuszS1jETe

对上述请求,各计算步骤的计算结果是:

  • $QUERY_PARAMS

    param1=value1&param2=value2
    
  • $HEADER_PARAMS

    user-agent=Random%20UA&x-customized-header=Customized-Value&x-wxgame-sign-appname=test_appname&x-wxgame-sign-method=WXGAME-TOKEN-HMAC-SHA256&x-wxgame-sign-nonce=BEBbaQtq&x-wxgame-sign-signedheaders=User-Agent%3BX-Customized-Header&x-wxgame-sign-timestamp=1713172261
    
  • $STRING_TO_SIGN

    POST
    /cgi-bin/comm/checksignature
    param1=value1&param2=value2
    user-agent=Random%20UA&x-customized-header=Customized-Value&x-wxgame-sign-appname=test_appname&x-wxgame-sign-method=WXGAME-TOKEN-HMAC-SHA256&x-wxgame-sign-nonce=BEBbaQtq&x-wxgame-sign-signedheaders=User-Agent%3BX-Customized-Header&x-wxgame-sign-timestamp=1713172261
    {}
    

    注:其中的换行符 \n 在上述示例中显示为真实的换行

  • $SIGN

    0f2dbfc9c7a7abd845fc08e800e560bd0a1d901b5c3eb4a84af7c1b239f93874
    

# Python 3 示例

import hmac
import hashlib
import time
import urllib.parse
import random
import string
from collections import OrderedDict
from http.client import HTTPSConnection


def calculate_signature(sign_token: str,
                        http_method: str,
                        http_uri: str,
                        query_params: dict[str, str],
                        header_params: dict[str, str],
                        http_body: bytes) -> str:
    # Calculate $QUERY_PARAMS
    ordered_query_params = OrderedDict(sorted(query_params.items()))
    query_string = '&'.join(
        [f"{urllib.parse.quote(k)}={urllib.parse.quote(v)}" for k, v in ordered_query_params.items()])
    print(f"QUERY-PARAMS: {query_string}")

    # Calculate $HEADER_PARAMS
    ordered_header_params = OrderedDict(sorted(header_params.items()))
    header_string = '&'.join(
        [f"{urllib.parse.quote(k.lower())}={urllib.parse.quote(v)}" for k, v in ordered_header_params.items()])
    print(f"HEADER-PARAMS: {header_string}")

    # Calculate $STRING_TO_SIGN
    string_to_sign = f"{http_method}\n{http_uri}\n{query_string}\n{header_string}\n"
    string_to_sign = string_to_sign.encode('utf-8')
    string_to_sign += http_body
    print(f"STRING-TO_SIGN: {repr(string_to_sign)}")

    # Calculate signature $SIGN
    sign = hmac.new(sign_token.encode(), string_to_sign, hashlib.sha256).hexdigest()
    print(f"SIGN: {sign}")

    return sign


def generate_random_string(length: int = 8) -> string:
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))


if __name__ == '__main__':
    sign_appname = "test_appname"
    sign_token = "O9ogYc5Dir40e4VyDAdIeTcuszS1jETe"
    http_method = "POST"
    http_uri = "/cgi-bin/comm/checksignature"
    query_params = {
        "param1": "value1",
        "param2": "value2"
    }

    header_params = {
        "X-WXGAME-SIGN-APPNAME": "test_appname",
        "X-WXGAME-SIGN-METHOD": "WXGAME-TOKEN-HMAC-SHA256",
        "X-WXGAME-SIGN-NONCE": generate_random_string(),
        "X-WXGAME-SIGN-TIMESTAMP": str(int(time.time())),
        "X-WXGAME-SIGN-SIGNEDHEADERS": "User-Agent;X-Customized-Header",
        "User-Agent": "Random UA",
        "X-Customized-Header": "Customized-Value"
    }

    http_body = b'{}'

    signature = calculate_signature(sign_token, http_method, http_uri, query_params, header_params, http_body)
    header_params["X-WXGAME-SIGN"] = signature

    # Make request
    # https://game.weixin.qq.com/cgi-bin/comm/checksignature
    conn = HTTPSConnection("game.weixin.qq.com")
    url = f"{http_uri}?{urllib.parse.urlencode(query_params, quote_via=urllib.parse.quote)}"
    conn.request(http_method, url, headers = header_params, body = http_body)
    response = conn.getresponse()
    print(response.status, response.reason)
    print(response.read())