Skip to content
Last updated

4.1 API 鉴权概述

所有商户 API(前缀 /v1/acquiring)必须携带以下 Header:

Header示例说明
DateTue, 21 Jan 2025 12:00:00 GMTGMT 格式服务器时间
AuthorizationSignature keyId="xxx"...HMAC 签名认证头

4.2 签名字符串(Signing String)

签名字符串(signing_string)格式如下:

{keyId}
{METHOD} {path}
date: {GMT_time}

示例:

merchant-001
POST /v1/acquiring/order
date: Tue, 21 Jan 2025 12:00:00 GMT

4.3 签名计算方法(HMAC-SHA256)

使用商户的 secret_key 对 signing_string 进行 HMAC-SHA256 计算,并进行 Base64 编码:

signature = base64.b64encode(
hmac.new(secret_key, signing_string.encode(), hashlib.sha256).digest()
).decode()

算法定义:

signature = Base64( HMAC_SHA256(secret_key, signing_string) )

4.4 Authorization Header 格式

完整的 Authorization Header:

Authorization: Signature keyId ="{keyId}", algorithm = "hmac-sha256", headers = "@request-target date",signature = "{signature}"

示例:

Authorization: Signature keyId = "merchant-001",
algorithm = "hmac-sha256",
headers= "@request-target date",
signature = "F0k29e...="

通用客户端示例

Python
class InfiniClient:
    def __init__(self, key_id, secret_key, base_url="https://openapi.infini.money"):
        self.key_id = key_id
        self.secret_key = secret_key.encode() if isinstance(secret_key, str) else secret_key
        self.base_url = base_url

    def _sign_request(self, method, path):
        gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')
        signing_string = f"{self.key_id}\n{method} {path}\ndate: {gmt_time}\n"
        signature = base64.b64encode(
            hmac.new(self.secret_key, signing_string.encode(), hashlib.sha256).digest()
        ).decode()
        return {
            "Date": gmt_time,
            "Authorization": f'Signature keyId="{self.key_id}",algorithm="hmac-sha256",'
                               f'headers="@request-target date",signature="{signature}"'
        }

    def request(self, method, path, json=None):
        headers = self._sign_request(method, path)
        if json is not None:
            headers["Content-Type"] = "application/json"
        response = requests.request(method, f"{self.base_url}{path}", json=json, headers=headers)
        response.raise_for_status()
        return response.json()
Nodejs
const crypto = require("crypto");
const axios = require("axios");

class InfiniClient {
    constructor(keyId, secretKey, baseUrl = "https://openapi.infini.money") {
        this.keyId = keyId;
        this.secretKey = secretKey; 
        this.baseUrl = baseUrl;
    }

    _signRequest(method, path) {
        const gmtTime = new Date().toUTCString();

        const signingString =
            `${this.keyId}\n` +
            `${method.toUpperCase()} ${path}\n` +
            `date: ${gmtTime}\n`;

        const signature = crypto
            .createHmac("sha256", this.secretKey)
            .update(signingString)
            .digest("base64");

        return {
            "Date": gmtTime,
            "Authorization":
                `Signature keyId="${this.keyId}",algorithm="hmac-sha256",headers="@request-target date",signature="${signature}"`
        };
    }

    async request(method, path, json = null) {
        const headers = this._signRequest(method, path);

        if (json !== null) {
            headers["Content-Type"] = "application/json";
        }

        const resp = await axios({
            method,
            url: `${this.baseUrl}${path}`,
            data: json,
            headers,
        });

        return resp.data;
    }
}

module.exports = InfiniClient;
Golang
package infiniclient

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

type InfiniClient struct {
	KeyID     string
	SecretKey string
	BaseURL   string
	Client    *http.Client
}

func NewInfiniClient(keyID, secretKey string, baseURL string) *InfiniClient {
	if baseURL == "" {
		baseURL = "https://openapi.infini.money"
	}
	return &InfiniClient{
		KeyID:     keyID,
		SecretKey: secretKey,
		BaseURL:   baseURL,
		Client:    &http.Client{Timeout: 15 * time.Second},
	}
}

func (c *InfiniClient) signRequest(method, path string) (map[string]string, error) {
	gmtTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")

	signingString := fmt.Sprintf(
		"%s\n%s %s\ndate: %s\n",
		c.KeyID,
		strings.ToUpper(method),
		path,
		gmtTime,
	)

	mac := hmac.New(sha256.New, []byte(c.SecretKey))
	mac.Write([]byte(signingString))

  
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	authHeader := fmt.Sprintf(
		`Signature keyId="%s",algorithm="hmac-sha256",headers="@request-target date",signature="%s"`,
		c.KeyID, signature,
	)

	return map[string]string{
		"Date":         gmtTime,
		"Authorization": authHeader,
	}, nil
}

func (c *InfiniClient) Request(method, path string, payload interface{}) (map[string]interface{}, error) {
	// Sign
	headers, err := c.signRequest(method, path)
	if err != nil {
		return nil, err
	}

	// Encode JSON body if provided
	var body io.Reader
	if payload != nil {
		b, err := json.Marshal(payload)
		if err != nil {
			return nil, err
		}
		body = bytes.NewBuffer(b)
		headers["Content-Type"] = "application/json"
	}

	// Build request
	req, err := http.NewRequest(method, c.BaseURL+path, body)
	if err != nil {
		return nil, err
	}

	// Set headers
	for k, v := range headers {
		req.Header.Set(k, v)
	}

	// Send
	resp, err := c.Client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	// Check status
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("request failed: %s, body=%s", resp.Status, string(bodyBytes))
	}

	// Decode JSON
	var data map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		return nil, err
	}

	return data, nil
}

4.5 时间偏差要求(Clock Skew)

请求头中的 Date 必须与服务器时间保持 ±300 秒以内误差;否则将返回:

401 Unauthorized

请确保服务器与 NTP 同步。

4.6 Webhook 签名验证(Webhook Verification)

Infini 向商户推送订单状态回调时,会附带签名。 商户需对签名进行校验,以确认消息来源可信并防止内容被篡改。

Webhook 请求包含以下 Header:

Header说明
X-Webhook-TimestampUnix 时间戳
X-Webhook-Event-Id本次事件唯一 ID
X-Webhook-SignatureHMAC-SHA256 签名值

4.6.1 Webhook 签名内容格式(Signing Content)

签名字符串格式:

{timestamp}.{event_id}.{payload_body}

示例:

1700000000.1234.{"event":"order.completed", "order_id":"xxx"}

计算方式:

expected_signature = HMAC_SHA256(webhook_secret, signing_content)

判断合法性:

X-Webhook-Signature == expected_signature

4.6.2 Webhook 验签示例(Python)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    event_id = request.headers.get('X-Webhook-Event-Id')

    if not all([signature, timestamp, event_id]):
        return {"error": "Missing required headers"}, 400

    payload = request.get_data(as_text=True)
    signed_content = f"{timestamp}.{event_id}.{payload}"

    expected_sig = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_content.encode(),
        hashlib.sha256
    ).hexdigest()

    if expected_sig != signature:
        return {"error": "Invalid signature"}, 401

    # Process webhook payload
    return {"status": "ok"}

4.7 安全最佳实践

  • 私钥(secret_key)仅展示一次,应立即安全备份。
  • 不得将 Secret Key 暴露在网页、JS、App 或公共仓库中。
  • 建议使用 KMS / Secret Manager 管理密钥。
  • 可在创建密钥时启用 IP 白名单限制访问来源。
  • Webhook 回调必须使用 HTTPS。
  • 建议定期轮换密钥(Key Rotation)。