外观
签名算法
所有客户端 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 | 请求方法,GET 或 POST |
host | API 域名,例如 api.nullverify.com |
path | 接口路径,例如 /api/client/v1/card/login |
k1=v1&...&kn=vn | 所有请求参数(不含 sign)按 key 字典序升序排列,以 & 连接 |
app_secret | 应用密钥 |
签名步骤
- 将所有请求参数(不含
sign)格式化为k=v形式 - 按 key 的字典序升序排列
- 用
&符号连接 - 拼接签名原文:
method + host + path + params + secret - 计算 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×tamp=1574654197"
# 拼接并计算
raw = http_method + host + path + params + app_secret
sign = hashlib.md5(raw.encode()).hexdigest()
print(sign) # 输出签名值注意
- 签名原文应使用参数的原始值,不要对 URL 编码后的字符串参与签名
sign参数本身不参与签名计算- 参数必须严格按照 key 的字典序排列
公共请求参数
所有客户端 API 请求都需要携带以下公共参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
app_key | string | 是 | 应用标识,在管理后台获取 |
timestamp | int | 是 | 当前时间戳(秒),签名在 60 秒内有效 |
nonce | string | 是 | 随机字符串(建议 UUID),90 秒内不可重复 |
sign | string | 是 | 请求签名 |
timestamp 说明
服务器会校验 timestamp 与当前服务器时间的差值,超过 60 秒的请求将被拒绝。请确保客户端时钟与服务器时间偏差不超过 1 分钟。
nonce 说明
nonce 用于防止重放攻击。对于同一应用,服务器会记录每次请求的 nonce,如果在 90 秒内出现重复的 nonce,请求将被拒绝。
响应签名(协议能力)
系统已实现响应签名算法,但当前客户端默认响应链路尚未启用 nonce 和 sign 字段输出。以下内容描述的是协议能力,待后续启用后生效。
响应签名公式
sign = md5(code + message + k1=v1&k2=v2&...&kn=vn + nonce + app_secret)| 参数 | 说明 |
|---|---|
code | 响应码,如 0 |
message | 响应消息,如 ok |
k1=v1&...&kn=vn | result 中所有字段按 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 |