跳到主要内容

签名认证调用指南

适用认证方式:开放接口 authType = app_secret(AppSecret 签名认证) 签名算法:默认 HMAC-SM3(国密),可选 HmacSHA256 本文的自检向量与平台网关实现一致,可直接用于校验你的签名实现。

认证方式概览​

authType名称说明需要携带的请求头
none无认证公开接口,直接放行-
app_key简单认证用 X-AppKey 与接口授权应用列表比对X-AppKey
app_secret签名认证AppKey + 时间戳 + 随机数 + HMAC 签名,防篡改、防重放X-AppKey、X-Timestamp、X-Nonce、X-Sign,可选 X-Algorithm
oauth2OAuth2 客户端凭证AppKey/AppSecret 换取短期 access_token,调用时携带 Bearer tokenAuthorization: Bearer <access_token>
jwt企业 IdP JWT企业 IdP 签发 token,平台只验签并按主体绑定识别应用Authorization: Bearer <enterprise_token>

AppKey / AppSecret 在平台「应用」中创建并授权接口后获取,AppSecret 需要妥善保管。

本文只描述 app_secret 签名认证。若接口认证方式为 oauth2,无需计算签名,凭 AppKey/AppSecret 换取 token 即可,详见 OAuth2 客户端凭证认证;若为 jwt,token 由企业 IdP 签发,详见 JWT 企业 IdP 认证。

签名认证逻辑​

请求头​

请求头必填说明
X-AppKey是应用的 AppKey
X-Timestamp是毫秒级时间戳(13 位)
X-Nonce是随机字符串,建议 16 字节强随机数的 hex(32 位)。同一 AppKey 在有效期内不可重复
X-Sign是签名结果(hex,大小写不敏感)
X-Algorithm否SM3(默认)或 HmacSHA256;其它值一律拒绝

签名串构造(核心)​

签名串 = HTTP_METHOD + "_" + API_PATH + "_" + AppKey + "_" + Timestamp + "_" + Nonce + "_" + 载荷摘要
载荷摘要 = SM3( 原始query串 ‖ 0x0A(换行字节) ‖ 请求体字节 )

各组成部分必须与实际发送的内容逐字一致,否则签名校验必然失败(40013):

组成部分取值要求
HTTP_METHOD大写方法名,如 GET / POST
API_PATH与平台配置的接口路径逐字一致,区分尾斜杠;网关按解码后的路径参与签名(%2F / %5C 直接拒绝,其它百分号编码会先解码再签名)
AppKey / Timestamp / Nonce与对应请求头的值完全一致(不要 trim、不要大小写转换)
原始 query 串实际发送的 query 串,不含 ?,且是未解码的原始编码形式。无 query 时为空串
请求体字节实际发送的 body 原始字节(UTF-8)。GET / HEAD、无 body 时为空字节
分隔符6 段用下划线 _ 连接;query 与 body 之间用一个换行字节 0x0A 分隔

计算 X-Sign​

默认: X-Sign = HMAC-SM3(key = AppSecret, message = 签名串)
可选: 请求头追加 X-Algorithm: HmacSHA256
X-Sign = HMAC-SHA256(key = AppSecret, message = 签名串)
注意

不是 SM3(AppSecret + 签名串)。裸 SM3 前缀拼接与 HMAC-SM3 的结果完全不同,且不具备 MAC 安全性。

自检向量​

向量 V1:GET,无 query、无请求体

项目值
AppSecretsk-test-1234567890abcdef
METHOD / PATHGET / /open/api/demo
AppKeyak_1234567890
Timestamp / Nonce1730000000000 / abc123def456
载荷摘要f4051d239b766c4111e92979aa31af0b35def053646e347bc41e8b73cfd080bc
X-Signfaf82457ec6890c750f15cd23127401a07ec51c56bac3b1b957e4ddc434831ed

向量 V2:POST,带 query 与中文请求体

项目值
AppSecretsk-test-1234567890abcdef
METHOD / PATHPOST / /open/api/demo
AppKeyak_1234567890
Timestamp / Nonce1730000002222 / n0nce-b
query 串 / 请求体page=1&size=20 / {"name":"张三","n":1}
载荷摘要71f7b849ada63301458054f9b5f748b99e3df834778ce7d9cb0fbe02f7ebeb42
X-Sign93ebde8a1023eaf8a60034521415c171bb15a20a1d148ad75480cec15134f280

接入前先用 V1/V2 向量自检你的签名函数,输出一致再联调。

网关校验流程​

失败返回 JSON(HTTP 状态码 200,业务码在 body.code):

{ "code": 40013, "msg": "签名验证失败,请检查验证参数是否正确" }

时间戳与 nonce 规则​

规则说明
时间戳窗口允许历史 10 分钟内的时间戳;允许最多 60 秒时钟超前(apigo.hmac.auth.timestamp-skew-millis 可调)
时间戳格式毫秒(13 位)。秒级时间戳或非数字会被判定为「已过期」
nonce每个请求必须唯一;网关以 AppKey + SM3(nonce) 做 SETNX,重复即视为重放
重试建议重试时必须重新生成 X-Timestamp 与 X-Nonce 并重新签名

配置项与限制​

项配置 / 限制说明
默认算法apigo.hmac.auth(默认 SM3)请求头未带 X-Algorithm 时使用
时钟超前容忍apigo.hmac.auth.timestamp-skew-millis(默认 60000)允许客户端时间戳超前的最大毫秒数
请求体参与签名上限apigo.auth.max-body-bytes(默认 1MB)超限直接返回 40013
支持的算法SM3、HmacSHA256HmacSHA1 等已下线
路径限制原始路径不允许出现编码后的路径分隔符(%2F、%5C)命中按「接口不存在」(40001)拒绝

调用示例​

脚本版(OpenSSL 3.x + curl)​

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://api.example.com"
API_PATH="/open/api/demo"
APP_KEY="ak_1234567890"
APP_SECRET="sk-test-1234567890abcdef"

METHOD="POST"
RAW_QUERY="page=1&size=20" # 实际发送的原始 query 串,无则留空
BODY='{"name":"张三","n":1}' # 实际发送的请求体,GET/HEAD 留空

TS="$(python3 -c 'import time; print(int(time.time()*1000))')"
NONCE="$(openssl rand -hex 16)" # 16 字节强随机,hex

hex_of() { printf '%s' "$1" | xxd -p | tr -d '\n'; }

# 载荷摘要 = SM3(原始query ‖ 0x0A ‖ 请求体字节)
DIGEST="$(printf '%s\n%s' "$RAW_QUERY" "$BODY" | openssl dgst -sm3 -binary | xxd -p | tr -d '\n')"

# 签名串 = METHOD_PATH_AppKey_Timestamp_Nonce_载荷摘要
SIGN_STRING="${METHOD}_${API_PATH}_${APP_KEY}_${TS}_${NONCE}_${DIGEST}"

# X-Sign = HMAC-SM3(key=AppSecret, message=签名串)
SIGN="$(printf '%s' "$SIGN_STRING" | openssl mac -macopt digest:SM3 -macopt hexkey:"$(hex_of "$APP_SECRET")" HMAC | tr 'A-F' 'a-f')"

URL="${BASE_URL}${API_PATH}"
[ -n "$RAW_QUERY" ] && URL="${URL}?${RAW_QUERY}"

curl -sS -X "$METHOD" "$URL" \
-H "Content-Type: application/json" \
-H "X-AppKey: ${APP_KEY}" \
-H "X-Timestamp: ${TS}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Sign: ${SIGN}" \
--data-binary "${BODY}"

Java(BouncyCastle)​

依赖:org.bouncycastle:bcprov-jdk18on。

import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Hex;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;

public class ApiSignDemo {

/** 生成 X-Sign:HMAC-SM3(AppSecret 作为密钥、签名串作为消息) */
public static String sign(String method, String path, String rawQuery, String body,
String appKey, String appSecret, String timestamp, String nonce) {
String data = method + "_" + path + "_" + appKey + "_" + timestamp + "_" + nonce + "_"
+ Hex.toHexString(payloadDigest(rawQuery, body));
return Hex.toHexString(hmacSm3(appSecret.getBytes(StandardCharsets.UTF_8),
data.getBytes(StandardCharsets.UTF_8)));
}

/** 载荷摘要 = SM3(原始 query 串 ‖ 0x0A ‖ 请求体字节) */
private static byte[] payloadDigest(String rawQuery, String body) {
SM3Digest digest = new SM3Digest();
byte[] queryBytes = rawQuery == null ? new byte[0] : rawQuery.getBytes(StandardCharsets.UTF_8);
digest.update(queryBytes, 0, queryBytes.length);
digest.update((byte) 0x0A);
byte[] bodyBytes = body == null ? new byte[0] : body.getBytes(StandardCharsets.UTF_8);
digest.update(bodyBytes, 0, bodyBytes.length);
byte[] out = new byte[digest.getDigestSize()];
digest.doFinal(out, 0);
return out;
}

private static byte[] hmacSm3(byte[] key, byte[] message) {
HMac mac = new HMac(new SM3Digest());
mac.init(new KeyParameter(key));
mac.update(message, 0, message.length);
byte[] out = new byte[mac.getMacSize()];
mac.doFinal(out, 0);
return out;
}

/** 16 字节强随机 nonce(hex) */
private static String randomNonce() {
byte[] bytes = new byte[16];
new SecureRandom().nextBytes(bytes);
return Hex.toHexString(bytes);
}
}

请求头组装:

HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("X-AppKey", APP_KEY)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Sign", sign)
.header("Content-Type", "application/json")
// 使用 HmacSHA256 时追加:.header("X-Algorithm", "HmacSHA256")
.method(method, HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();

Python(gmssl + requests)​

依赖:pip install gmssl requests。

import hashlib, hmac, time, uuid
from urllib.parse import quote, urlencode
import requests
from gmssl import sm3


def sm3_hex(data: bytes) -> str:
return sm3.sm3_hash(list(data))


def hmac_sm3_hex(key: bytes, message: bytes) -> str:
"""HMAC-SM3:SM3 分组长度 64 字节,密钥超长时先做一次 SM3 压缩。"""
if len(key) > 64:
key = bytes.fromhex(sm3_hex(key))
key = key.ljust(64, b"\x00")
ipad_key = bytes(b ^ 0x36 for b in key)
opad_key = bytes(b ^ 0x5C for b in key)
inner = bytes.fromhex(sm3_hex(ipad_key + message))
return sm3_hex(opad_key + inner)


def payload_digest(raw_query: str, body_bytes: bytes) -> str:
"""载荷摘要 = SM3(原始 query 串 ‖ 0x0A ‖ 请求体字节),query 与 body 都按 UTF-8 字节参与计算"""
query_bytes = (raw_query or "").encode("utf-8")
return sm3_hex(query_bytes + b"\n" + (body_bytes or b""))


def sign(method, path, raw_query, body_bytes, app_key, app_secret, timestamp, nonce):
data = f"{method}_{path}_{app_key}_{timestamp}_{nonce}_{payload_digest(raw_query, body_bytes)}"
return hmac_sm3_hex(app_secret.encode("utf-8"), data.encode("utf-8"))


def call_api(base_url, path, app_key, app_secret, method="GET",
query_params=None, body=None):
# 1) 构造"将要发送的"原始 query 串:签名与请求共用同一份字符串
raw_query = urlencode(query_params, quote_via=quote) if query_params else ""
# 2) 请求体统一转成 UTF-8 字节:签名与发送必须是同一份字节,否则中文等非 ASCII 内容会因编码差异验签失败
body_bytes = body.encode("utf-8") if isinstance(body, str) else (body or b"")
timestamp = str(int(time.time() * 1000))
nonce = uuid.uuid4().hex
sign_value = sign(method, path, raw_query, body_bytes, app_key, app_secret, timestamp, nonce)

headers = {
"X-AppKey": app_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Sign": sign_value,
# 使用 HmacSHA256 时追加:'X-Algorithm': 'HmacSHA256'
}
if body_bytes:
headers["Content-Type"] = "application/json"
url = base_url + path + (f"?{raw_query}" if raw_query else "")
# data=body_bytes 原样发送已签名的字节,避免 HTTP 库重新序列化请求体;无 body 时传 None
return requests.request(method, url, headers=headers, data=body_bytes or None, timeout=10)

Go(gmsm)​

依赖:github.com/emmansun/gmsm/sm3。

package main

import (
"crypto/hmac"
"encoding/hex"
"fmt"
"strings"

"github.com/emmansun/gmsm/sm3"
)

const (
appKey = "ak_1234567890"
appSecret = "sk-test-1234567890abcdef"
)

// sign 生成 X-Sign:HMAC-SM3(AppSecret 作为密钥、签名串作为消息)
func sign(method, path, rawQuery, body, timestamp, nonce string) string {
data := strings.Join([]string{method, path, appKey, timestamp, nonce, payloadDigest(rawQuery, []byte(body))}, "_")
mac := hmac.New(sm3.New, []byte(appSecret))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}

// payloadDigest 载荷摘要 = SM3(原始 query 串 ‖ 0x0A ‖ 请求体字节)
func payloadDigest(rawQuery string, body []byte) string {
h := sm3.New()
h.Write([]byte(rawQuery))
h.Write([]byte{'\n'})
h.Write(body)
return hex.EncodeToString(h.Sum(nil))
}

func main() {
fmt.Println(sign("POST", "/open/api/demo", "page=1&size=20",
`{"name":"张三","n":1}`, "1730000002222", "n0nce-b"))
}

Node.js / 浏览器(sm-crypto)​

// npm install sm-crypto
const sm3 = require('sm-crypto').sm3

const APP_KEY = 'ak_1234567890'
const APP_SECRET = 'sk-test-1234567890abcdef'

/** UTF-8 字节数组(sm-crypto 的 hmac 模式要求 key 传字节数组) */
function utf8Bytes(text) {
return Array.from(new TextEncoder().encode(text || ''))
}

/** 载荷摘要 = SM3(原始 query 串 ‖ 0x0A ‖ 请求体) */
function payloadDigest(rawQuery, bodyText) {
return sm3([...utf8Bytes(rawQuery), 0x0A, ...utf8Bytes(bodyText)])
}

/** X-Sign = HMAC-SM3(AppSecret 作为密钥、签名串作为消息) */
function sign(method, path, rawQuery, bodyText, timestamp, nonce) {
const data = `${method}_${path}_${APP_KEY}_${timestamp}_${nonce}_${payloadDigest(rawQuery, bodyText)}`
return sm3(data, { key: utf8Bytes(APP_SECRET), mode: 'hmac' })
}

/** 16 字节强随机 nonce(hex) */
function createNonce() {
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
}

HmacSHA256 变体:请求头必须带 X-Algorithm: HmacSHA256,签名改用 SHA256 的 HMAC 实现;载荷摘要仍固定用 SM3。

常见错误与排查​

错误码​

code含义常见原因
40008X-AppKey 不能为空未携带 X-AppKey
40009认证参数不能为空X-AppKey / X-Timestamp / X-Nonce / X-Sign 有空值
40010时间戳已过期 / nonce 已失效时间戳超出窗口、传了秒级时间戳;或 nonce 重复(重放)
40013签名验证失败签名串构造不一致、算法不匹配、AppSecret 错误、请求体超限
40002应用无接口权限该接口未授权给此应用
40018IP 不在授权范围应用配置了凭证 IP 白名单,来源 IP 不在其中
40019应用已禁用应用被停用;凭证状态校验在验签前执行,签名正确也会被拒绝
40001接口不存在或已下线路径不一致;或路径含 %2F / %5C
40023日调用量配额已超出当日配额用尽
40017请求包含非法字符(WAF 拦截)返回 HTTP 400(其它认证错误为 HTTP 200 + body.code)

40013 排查清单(按出现频率排序)​

  1. query 串没纳入签名,或签名用的 query 与发送的不一致(最常见)。%20 与 +、%2F 与 /、参数顺序、是否带 ? 都必须一致。
  2. 请求体被 HTTP 库重新序列化。签名必须是「最终发出的那份字节」:Python 用 data=body_bytes 不用 json=;Node 传同一份字符串;Java/Go 先序列化成字节数组再同时用于签名与发送。
  3. 载荷摘要拼接漏了换行:query 与 body 之间必须有一个 0x0A 字节,即使都为空(GET 无 body 时摘要 = SM3(0x0A))。
  4. 算法不匹配:用 HMAC-SHA256 签名却没带 X-Algorithm: HmacSHA256。
  5. 误用 SM3(AppSecret + 签名串):正确算法是 HMAC-SM3(AppSecret, 签名串)。
  6. 路径不一致:含尾斜杠、大小写差异。
  7. 时钟偏差:服务器/客户端时间不同步导致 40010。
  8. AppSecret 不正确:用了其它应用的密钥,或复制带了空格。

快捷自检手段​

  • 先跑自检向量 V1/V2,确认算法实现无误;
  • 用平台「接口详情 → 请求参数 → 在线签名」生成签名与自己的实现对比;
  • 用「接口详情 → 在线测试」直接发起真实调用,快速区分「签名问题」与「链路问题」。

安全建议​

  1. AppSecret 只放在服务端,禁止出现在浏览器、App 包、日志或前端代码中;平台「接口详情 → 在线签名」仅在浏览器本地完成 HMAC 计算、不会上传,可用于联调自查
  2. 走 HTTPS:签名只保证完整性与防重放,不提供机密性
  3. nonce 用强随机,不要用时间戳或自增序号代替
  4. 不要复用 (Timestamp, Nonce):重试重新生成并重新签名
  5. 按 IP 白名单收敛调用来源
  6. 定期轮换 AppSecret,遵循最小授权原则
  7. 验签失败不要盲目重试:除时钟偏差外,40013 基本都是代码问题

附:签名规范速查卡​

① 准备:AppKey / AppSecret(应用创建并授权接口后获取)
② 生成:Timestamp = 当前毫秒时间戳(13 位)
Nonce = 16 字节强随机 hex(唯一即可)
③ 摘要:PayloadDigest = SM3( rawQuery ‖ 0x0A ‖ bodyBytes )
④ 签名串:SignString = METHOD_API-PATH_AppKey_Timestamp_Nonce_PayloadDigest
⑤ 签名:X-Sign = HMAC-SM3(AppSecret, SignString) // 默认
HMAC-SHA256(AppSecret, SignString) // 需带 X-Algorithm: HmacSHA256
⑥ 请求头:X-AppKey / X-Timestamp / X-Nonce / X-Sign [ / X-Algorithm ]
⑦ 自检向量:见上文