### 4.1 API 鉴权概述 所有商户 API(前缀 /v1/acquiring)必须携带以下 Header: | Header | 示例 | 说明 | | --- | --- | --- | | Date | Tue, 21 Jan 2025 12:00:00 GMT | GMT 格式服务器时间 | | Authorization | Signature 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 编码: ```python 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 ```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 ```javascript 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 ```go 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-Timestamp | Unix 时间戳 | | X-Webhook-Event-Id | 本次事件唯一 ID | | X-Webhook-Signature | HMAC-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) ```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)。