接口鉴权说明
为保障通信安全,所有对本平台 API 的请求都必须经过严格的身份验证。本文档将详细说明如何生成请求签名以及如何通过验证。
准备工作:获取 API 凭证
在开始之前,请登录您的商户管理面板,在 API 设置中获取以下两项关键凭证:
API_KEY
:您的唯一身份标识符。SECRET_KEY
:用于计算签名的私钥,请务必妥善保管,切勿泄露。
鉴权流程核心
鉴权核心是客户端使用 SECRET_KEY
对请求的关键信息生成一个签名 (signature
),然后将 API_KEY
、signature
及其他辅助信息通过 HTTP 请求头 (Headers
) 发送给服务器。服务器会以完全相同的方式在本地重新计算签名,并与客户端传来的签名进行比对,若一致则验证通过。
构建待签名字符串
签名过程的第一步是按照以下规则,将请求的多个部分组合成一个单一的、规范化的字符串。我们称之为 stringToSign
。
stringToSign
的结构如下,各部分之间以换行符 \n
分隔:
REQUEST_BODY\n
TIMESTAMP\n
NONCE
各组成部分的详细规则:
变量 | 说明 | 规则与说明 |
---|---|---|
REQUEST_BODY | 请求体 | HTTP 请求的 Body 内容原文。注意事项:1. 对于 GET 等无请求体的请求,此部分为空字符串。2. 对于 POST 、PUT 请求,此部分为请求体的原始字节流。请确保不对其进行任何修改(如添加/删除空格、改变JSON字段顺序等)。 |
TIMESTAMP | 时间戳 | 当前的 Unix 时间戳(秒级),表示为字符串。服务器会拒绝与服务器时间相差超过 ±5分钟 的请求,以防止重放攻击。 |
NONCE | 随机数 | 一个只使用一次的随机字符串,用于防止重放攻击。要求:1. 建议使用 UUID 或其他高强度随机字符串生成。2. 对于同一个 API_KEY ,在 TIMESTAMP 定义的有效时间窗口内,NONCE 必须是唯一的。服务器会拒绝处理已经接收过的 NONCE 。 |
重要提示
构建 stringToSign
时,请确保所有文本内容均使用 UTF-8 编码。
计算签名
- 使用
HMAC-SHA256
算法,以您的SECRET_KEY
作为密钥,对上一步构建的stringToSign
进行哈希计算。 - 将计算得到的二进制哈希值进行 十六进制 (Hex) 编码。
- 将编码后的十六进制字符串转换为小写,得到最终的
signature
。
伪代码示例:
python
signature = hmac_sha256(secret_key, stringToSign).hex().lower()
发送 API 请求
将以下鉴权信息放入 HTTP 请求的 Header 中:
Header 字段 | 值 | 来源 |
---|---|---|
X-Api-Key | 您的 API_KEY | 来自商户面板 |
X-Timestamp | 使用的时间戳 | 与 stringToSign 中的 TIMESTAMP 保持一致 |
X-Nonce | 使用的随机数 | 与 stringToSign 中的 NONCE 保持一致 |
X-Signature | 计算出的签名 | signature |
完整示例
假设我们有以下凭证和请求信息:
- API_KEY:
3AUpfeK573UH5vVe
- SECRET_KEY:
5ShtY7nXAT8Wm2RBeKLv7iPakVyxjddU
- 请求:
http
POST /openapi/v1/payment HTTP/1.1
Content-Type: application/json
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}
构建 stringToSign
- REQUEST_BODY:
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}
- TIMESTAMP: 假设为
1754574105
- NONCE: 假设为
random_nonce_str
将以上各部分用 \n
拼接,得到 stringToSign
(为了可读性,这里的 \n
仅作示意):
text
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}\n1754574105\nrandom_nonce_str
计算 signature
使用 HMAC-SHA256
算法,以 5ShtY7nXAT8Wm2RBeKLv7iPakVyxjddU
为密钥,对上述 stringToSign
进行计算。得到的签名为 ce4f73fcc17722e053f7315bfa48384bc50e579ec760e71fa91a6f7cf0d24bfa
。
组装并发送请求
bash
POST /openapi/v1/payment HTTP/1.1
Host: api.zaepe.com
Content-Type: application/json
X-Api-Key: 3AUpfeK573UH5vVe
X-Nonce: random_nonce_str
X-Signature: ce4f73fcc17722e053f7315bfa48384bc50e579ec760e71fa91a6f7cf0d24bfa
X-Timestamp: 1754574105
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}
参考代码
Golang
go
package apisign
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/cockroachdb/errors"
)
const (
HeaderAPIKey = "X-Api-Key"
HeaderTimestamp = "X-Timestamp"
HeaderNonce = "X-Nonce"
HeaderSignature = "X-Signature"
// DefaultExpiration defines the default expiration time for a signature.
DefaultExpiration = 30 * time.Second
)
type SignError struct {
Field string
Err error
}
func (e *SignError) Error() string {
return e.Field + ": " + e.Err.Error()
}
func Verify(req *http.Request, secret string) error {
if key := req.Header.Get(HeaderAPIKey); key == "" {
return &SignError{Field: HeaderAPIKey, Err: errors.New("empty api key")}
}
timestamp := req.Header.Get(HeaderTimestamp)
if timestamp == "" {
return &SignError{Field: HeaderTimestamp, Err: errors.New("empty timestamp")}
}
unix, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return &SignError{Field: HeaderTimestamp, Err: errors.New("invalid timestamp")}
}
t := time.Unix(unix, 0)
if time.Since(t).Abs() > DefaultExpiration {
return &SignError{Field: HeaderTimestamp, Err: errors.New("timestamp expired")}
}
nonce := req.Header.Get(HeaderNonce)
if nonce == "" {
return &SignError{Field: HeaderNonce, Err: errors.New("empty nonce")}
}
signature := strings.ToLower(req.Header.Get(HeaderSignature))
if signature == "" {
return &SignError{Field: HeaderSignature, Err: errors.New("empty signature")}
}
b, err := io.ReadAll(req.Body)
if err != nil {
return &SignError{Field: HeaderSignature, Err: errors.Wrap(err, "fail to read body")}
}
req.Body = io.NopCloser(bytes.NewReader(b))
sign := Sign(secret, b, t, nonce)
if !hmac.Equal([]byte(sign), []byte(signature)) {
return &SignError{Field: HeaderSignature, Err: errors.New("invalid signature")}
}
return nil
}
// Sign generates a signature for the given request body, timestamp, and nonce.
// The signature is an HMAC-SHA256 hash of the request body, timestamp, and nonce, separated by newlines.
func Sign(secretKey string, requestBody []byte, t time.Time, nonce string) string {
// For debugging, you can easily print the content to be signed.
// log.Printf("signing content: %s", string(content))
content := bytes.Join(
[][]byte{
requestBody,
[]byte(strconv.FormatInt(t.Unix(), 10)),
[]byte(nonce),
},
[]byte("\n"),
)
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write(content)
return hex.EncodeToString(mac.Sum(nil))
}
PHP
php
class SignError extends \Exception
{
/**
* @var string
*/
protected $field;
public function __construct(string $field, string $message = "", int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->field = $field;
}
public function getField(): string
{
return $this->field;
}
public function __toString(): string
{
return $this->field . ": " . $this->getMessage();
}
}
class ApiSigner
{
public static function verify(\Psr\Http\Message\ServerRequestInterface $request, string $secret): bool
{
$key = $request->getHeaderLine('X-Api-Key');
if (empty($key)) {
throw new SignError("X-Api-Key", "empty api key");
}
$timestamp = $request->getHeaderLine('X-Timestamp');
if (empty($timestamp)) {
throw new SignError("X-Timestamp", "empty timestamp");
}
$unixTimestamp = (int)$timestamp;
if ((string)$unixTimestamp !== $timestamp) {
throw new SignError("X-Timestamp", "invalid timestamp");
}
$nonce = $request->getHeaderLine('X-Nonce');
if (empty($nonce)) {
throw new SignError("X-Nonce", "empty nonce");
}
$signature = strtolower($request->getHeaderLine('X-Signature'));
if (empty($signature)) {
throw new SignError("X-Signature", "empty signature");
}
$body = (string)$request->getBody();
$request->getBody()->rewind();
$sign = self::sign(
$secret,
$body,
$unixTimestamp,
$nonce
);
if ($sign !== $signature) {
throw new SignError("X-Signature", "invalid signature");
}
return true;
}
public static function sign(
string $secretKey,
string $requestBody,
int $timestamp,
string $nonce
): string {
$data = implode("\n", [
$requestBody,
(string)$timestamp,
$nonce,
]);
return hash_hmac('sha256', $data, $secretKey);
}
}
常见问题
请严格按照以下顺序排查:
SECRET_KEY
是否正确无误?stringToSign
的拼接顺序是否完全正确?REQUEST_BODY
是否与实际发送的字节完全一致?(注意检查多余的空格或换行)TIMESTAMP
是否是秒级时间戳,且在服务器允许的时间窗口内?- 所有参与拼接的字符串是否都使用了 UTF-8 编码?