Skip to content

签名算法

所有客户端 API 请求都需要携带签名参数,用于验证请求的合法性和完整性。NullVerify 支持 MD5 和 HMAC-SHA256 两种签名算法。

请求签名

MD5 签名(默认)

sign = md5(http_method + host + path + k1=v1&k2=v2&...&kn=vn + app_secret)

HMAC-SHA256 签名

sign = hmac_sha256(http_method + host + path + k1=v1&k2=v2&...&kn=vn, app_secret)

说明

HMAC-SHA256 模式下,app_secret 作为密钥而非拼接到原始字符串中。

参数说明

参数说明
http_method请求方法,GETPOST
hostAPI 域名,例如 api.nullverify.com
path接口路径,例如 /api/client/v1/card/login
k1=v1&...&kn=vn所有请求参数(不含 sign)按 key 字典序升序排列,以 & 连接
app_secret应用密钥

签名步骤

  1. 将所有请求参数(不含 sign)格式化为 k=v 形式
  2. 按 key 的字典序升序排列
  3. & 符号连接
  4. 拼接签名原文:method + host + path + params + secret
  5. 计算 MD5 哈希值(小写十六进制)

签名示例

以卡密登录接口为例:

python
import hashlib

app_secret = "uiS9M0G8JolpUvlf5NxZ7pwMVinKs73x"
http_method = "POST"
host = "api.nullverify.com"
path = "/api/client/v1/card/login"

# 参数按 key 字典序排列
params = "app_key=blsvh14llhcr96vtboqg&card=abc3b65KDZ9Qb7UC685D2MVFR0TPc53BCU1IPD5ad20&device_id=123&nonce=359c22e4-d522-4771-ba8e-4b99cf61b372&timestamp=1574654197"

# 拼接并计算
raw = http_method + host + path + params + app_secret
sign = hashlib.md5(raw.encode()).hexdigest()
print(sign)  # 输出签名值

注意

  • 签名原文应使用参数的原始值,不要对 URL 编码后的字符串参与签名
  • sign 参数本身不参与签名计算
  • 参数必须严格按照 key 的字典序排列

公共请求参数

所有客户端 API 请求都需要携带以下公共参数:

参数名类型必填说明
app_keystring应用标识,在管理后台获取
timestampint当前时间戳(秒),签名在 60 秒内有效
noncestring随机字符串(建议 UUID),90 秒内不可重复
signstring请求签名

timestamp 说明

服务器会校验 timestamp 与当前服务器时间的差值,超过 60 秒的请求将被拒绝。请确保客户端时钟与服务器时间偏差不超过 1 分钟。

nonce 说明

nonce 用于防止重放攻击。对于同一应用,服务器会记录每次请求的 nonce,如果在 90 秒内出现重复的 nonce,请求将被拒绝。

响应签名(协议能力)

系统已实现响应签名算法,但当前客户端默认响应链路尚未启用 noncesign 字段输出。以下内容描述的是协议能力,待后续启用后生效。

响应签名公式

sign = md5(code + message + k1=v1&k2=v2&...&kn=vn + nonce + app_secret)
参数说明
code响应码,如 0
message响应消息,如 ok
k1=v1&...&kn=vnresult 中所有字段按 key 字典序排列
nonce服务端返回的随机字符串,保证唯一且趋势递增
app_secret应用密钥

验签示例

python
import hashlib

app_secret = "uiS9M0G8JolpUvlf5NxZ7pwMVinKs73x"
prev_nonce = None  # 记录上次服务端返回的 nonce

def check_response_sign(resp):
    global prev_nonce

    # 校验 nonce 递增
    if prev_nonce is not None and resp["nonce"] <= prev_nonce:
        return False  # nonce 未递增,可能是重放
    prev_nonce = resp["nonce"]

    # 构建 result 参数字符串
    result = resp.get("result", {})
    params = sorted(f"{k}={v}" for k, v in result.items())
    param_str = "&".join(params)

    # 拼接并计算签名
    raw = f"{resp['code']}{resp['message']}{param_str}{resp['nonce']}{app_secret}"
    expected_sign = hashlib.md5(raw.encode()).hexdigest()

    return expected_sign == resp["sign"]

响应示例

json
{
  "code": 0,
  "message": "ok",
  "result": {
    "expires": "2024-12-31 23:59:59",
    "expires_ts": 1735689599,
    "server_time": 1703001600
  },
  "nonce": "bojc2kiuof2jci9b90jg",
  "sign": "4954c9805d4040a95336150e6e5f14e2"
}

常见签名问题排查

问题排查方向
签名不一致 (10010)检查参数排序是否正确、参数值是否进行了 URL encode、app_secret 是否正确
签名已过期 (10011)检查客户端时钟是否准确,timestamp 与服务器时差不超过 60 秒
时间戳异常 (10013)timestamp 大于服务器当前时间,检查客户端时钟
nonce 重复 (10014)确保每次请求使用不同的 nonce,建议使用 UUID

面向脚本与插件开发者的网络验证系统