# 服务端 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
- 将 QueryString 中所有的 Key 取出来按字典序排序
- 按 Key 排序后的顺序,从 QueryString 取出对应的 Value ,拼成
UrlEncode(key)=UrlEncode(value)
键值对,其中UrlEncode
按 encodeURIComponent 规范实现 - 把上述所有键值对按顺序用
"&"
连接起来
$QUERY_PARAMS = UrlEncode($key1) + "=" + UrlEncode($value1)
+ "&" + UrlEncode($key2) + "=" + UrlEncode($value2)
+ ...
# 2. 计算 $HEADER_PARAMS
- 将上述除了
X-WXGAME-SIGN
外的认证用 Header 与X-WXGAME-SIGN-SIGNEDHEADERS
指明的 Header Key 共同放在一个列表中 注:在 HTTP 请求中包含的 Header 才需要参与计算签名 - 把列表中所有的 Header Key 转为小写,并按字典序排序
- 按小写 Key 排序后的顺序,从 Header 中取出对应的 Value ,拼成
UrlEncode(key_lowercase)=UrlEncode(value)
键值对,其中UrlEncode
按 encodeURIComponent 规范实现 - 把上述所有键值对按顺序用
"&"
连接起来,其中 Key 用的是小写后的 Header Key
$HEADER_PARAMS = UrlEncode($key1_lowercase) + "=" + UrlEncode(&value1)
+ "&" + UrlEncode($key2_lowercase) + "=" + UrlEncode($value2)
+ ...
# 3. 计算 $STRING_TO_SIGN
- 获得 HTTP 请求的方法名
$HTTP_METHOD
,如GET
,POST
- 获得 HTTP 请求的 URI
$HTTP_URI
,如/cgi-bin/some/api
- 获得 HTTP 请求的 Body
$HTTP_BODY
,若无则为空
$STRING_TO_SIGN = $HTTP_METHOD + "\n"
+ $HTTP_URI + "\n"
+ $QUERY_PARAMS + "\n"
+ $HEADER_PARAMS + "\n"
+ $HTTP_BODY
# 4. 计算签名 $SIGN
- 当前支持的算法是
WXGAME-TOKEN-HMAC-SHA256
,签名时使用HMAC-SHA256
算法 $SIGN = HMAC-SHA256(key = $sign_token, value = $STRING_TO_SIGN)
- 其中使用的
$sign_token
为预分配密钥,每个sign_appname
对应一个密钥
# 签名示例
curl -XPOST "https://game.weixin.qq.com/cgi-bin/comm/checksignature?param1=value1¶m2=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¶m2=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¶m2=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())