{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-@l10n/zh/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"4. 授权与安全机制","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"4-授权与安全机制","__idx":0},"children":["4. 授权与安全机制"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Infini API 使用基于 ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["HMAC-SHA256"]}," 的签名认证机制，确保商户与 Infini 之间的通信安全可靠、不可伪造、不可篡改。"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["授权机制主要包含以下要素："]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["API Key（keyId）"]},"：商户唯一身份标识，用于在请求头中标识调用方身份。keyId 就是在后台生成的 public key。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Secret Key（私钥）"]},"：用于对请求内容进行 HMAC-SHA256 签名，只展示一次，必须妥善保存。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["签名字符串（Signing String）"]},"：由 keyId、HTTP 方法、请求路径与 GMT 时间组成。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Authorization Header"]},"：最终的签名头，用于服务端验证请求合法性。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["时间戳要求"]},"：客户端时间需与服务器保持 ±300 秒以内偏差。"]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Secret Key 必须存放在后端安全环境（如 KMS），不得暴露给前端或移动端。"]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"41-api-鉴权概述","__idx":1},"children":["4.1 API 鉴权概述"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["所有商户 API（前缀 /v1/acquiring）必须携带以下 Header："]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Header"},"children":["Header"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"示例"},"children":["示例"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"说明"},"children":["说明"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Date"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Tue, 21 Jan 2025 12:00:00 GMT"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["GMT 格式服务器时间"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Digest"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["SHA-256=47DEQpj8...uFU="]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["请求体摘要（有 Body 时必须携带）"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Authorization"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Signature keyId=\"xxx\"..."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["HMAC 签名认证头"]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"42-签名字符串signing-string","__idx":2},"children":["4.2 签名字符串（Signing String）"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["签名字符串（signing_string）格式如下（有无 Body 的请求格式相同）："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"{keyId}\n{METHOD} {path}\ndate: {GMT_time}\n"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["注意"]},"：body/digest 不参与签名计算。Digest header 需要单独计算并添加到请求头中，但不包含在签名字符串中。"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["示例（无 Body）："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"merchant-001\nGET /v1/acquiring/order?order_id=xxx\ndate: Tue, 21 Jan 2025 12:00:00 GMT\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["示例（有 Body）："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"merchant-001\nPOST /v1/acquiring/order\ndate: Tue, 21 Jan 2025 12:00:00 GMT\n"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"43-摘要和签名计算digest--signature-calculation","__idx":3},"children":["4.3 摘要和签名计算（Digest & Signature Calculation）"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"body-digest-计算有-body-时","__idx":4},"children":["Body Digest 计算（有 Body 时）"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"body_digest = hashlib.sha256(body.encode('utf-8')).digest()\nbody_digest_base64 = base64.b64encode(body_digest).decode('utf-8')\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["算法定义："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"body_digest_base64 = Base64( SHA256(body) )\n"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"signature-计算所有请求","__idx":5},"children":["Signature 计算（所有请求）"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["使用商户的 secret_key 对 signing_string 进行 HMAC-SHA256 计算："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"signature = base64.b64encode(\nhmac.new(secret_key, signing_string.encode(), hashlib.sha256).digest()\n).decode()\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["算法定义："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"signature = Base64( HMAC_SHA256(secret_key, signing_string) )\n"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"44-http-headers-格式","__idx":6},"children":["4.4 HTTP Headers 格式"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["无 Body 请求："]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"Date: {GMT_time}\nAuthorization: Signature keyId=\"{keyId}\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"{signature}\"\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["有 Body 请求："]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"Date: {GMT_time}\nDigest: SHA-256={body_digest_base64}\nAuthorization: Signature keyId=\"{keyId}\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"{signature}\"\n"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"通用客户端示例","__idx":7},"children":["通用客户端示例"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":5,"id":"python","__idx":8},"children":["Python"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"\nfrom __future__ import annotations\n\nimport base64\nimport hashlib\nimport hmac\nimport json as json_lib\nfrom datetime import datetime, timezone\nfrom email.utils import format_datetime\n\nimport requests\n\n\ndef _gmt_rfc1123() -> str:\n    return format_datetime(datetime.now(timezone.utc), usegmt=True)\n\n\nclass InfiniClient:\n    def __init__(self, key_id: str, secret_key: str | bytes, base_url: str = \"https://openapi.infini.money\"):\n        self.key_id = key_id\n        self.secret_key = secret_key.encode() if isinstance(secret_key, str) else secret_key\n        self.base_url = base_url.rstrip(\"/\")\n\n    def _sign_request(self, method: str, path: str, body: str | None, *, digest: bool) -> dict[str, str]:\n        gmt_time = _gmt_rfc1123()\n        signing_string = f\"{self.key_id}\\n{method} {path}\\ndate: {gmt_time}\\n\"\n        signature = base64.b64encode(\n            hmac.new(self.secret_key, signing_string.encode(), hashlib.sha256).digest()\n        ).decode()\n        headers: dict[str, str] = {\n            \"Date\": gmt_time,\n            \"Authorization\": (\n                f'Signature keyId=\"{self.key_id}\",algorithm=\"hmac-sha256\",'\n                f'headers=\"@request-target date\",signature=\"{signature}\"'\n            ),\n        }\n        if digest and body is not None:\n            d = hashlib.sha256(body.encode(\"utf-8\")).digest()\n            headers[\"Digest\"] = \"SHA-256=\" + base64.b64encode(d).decode(\"ascii\")\n        return headers\n\n    def request(\n        self,\n        method: str,\n        path: str,\n        json: dict | None = None,\n        *,\n        digest: bool = False,\n        timeout: float = 30.0,\n    ) -> dict:\n        body: str | None = None\n        if json is not None:\n            body = json_lib.dumps(json, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False)\n\n        headers = self._sign_request(method, path, body, digest=digest)\n        if json is not None:\n            headers[\"Content-Type\"] = \"application/json\"\n\n        r = requests.request(\n            method,\n            f\"{self.base_url}{path}\",\n            data=body,\n            headers=headers,\n            timeout=timeout,\n        )\n        r.raise_for_status()\n        return r.json()\n\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":5,"id":"nodejs","__idx":9},"children":["Nodejs"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const crypto = require(\"crypto\");\nconst axios = require(\"axios\");\n\nclass InfiniClient {\n    constructor(keyId, secretKey, baseUrl = \"https://openapi.infini.money\") {\n        this.keyId = keyId;\n        this.secretKey = secretKey; \n        this.baseUrl = baseUrl;\n    }\n\n    _signRequest(method, path, body = null) {\n        const gmtTime = new Date().toUTCString();\n\n        const signingString =\n            `${this.keyId}\\n` +\n            `${method.toUpperCase()} ${path}\\n` +\n            `date: ${gmtTime}\\n`;\n\n        const signature = crypto\n            .createHmac(\"sha256\", this.secretKey)\n            .update(signingString)\n            .digest(\"base64\");\n\n        const headers = {\n            \"Date\": gmtTime,\n            \"Authorization\":\n                `Signature keyId=\"${this.keyId}\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"${signature}\"`\n        };\n\n        // 如果有 body，计算并添加 Digest header（不参与签名）\n        if (body !== null) {\n            const bodyDigest = crypto.createHash(\"sha256\").update(body, \"utf-8\").digest();\n            const bodyDigestBase64 = bodyDigest.toString(\"base64\");\n            headers[\"Digest\"] = `SHA-256=${bodyDigestBase64}`;\n        }\n\n        return headers;\n    }\n\n    async request(method, path, json = null) {\n        let body = null;\n        if (json !== null) {\n            body = JSON.stringify(json);\n        }\n\n        const headers = this._signRequest(method, path, body);\n\n        if (json !== null) {\n            headers[\"Content-Type\"] = \"application/json\";\n        }\n\n        const resp = await axios({\n            method,\n            url: `${this.baseUrl}${path}`,\n            data: body,\n            headers,\n        });\n\n        return resp.data;\n    }\n}\n\nmodule.exports = InfiniClient;\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":5,"id":"golang","__idx":10},"children":["Golang"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"go","header":{"controls":{"copy":{}}},"source":"package infiniclient\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype InfiniClient struct {\n\tKeyID     string\n\tSecretKey string\n\tBaseURL   string\n\tClient    *http.Client\n}\n\nfunc NewInfiniClient(keyID, secretKey string, baseURL string) *InfiniClient {\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://openapi.infini.money\"\n\t}\n\treturn &InfiniClient{\n\t\tKeyID:     keyID,\n\t\tSecretKey: secretKey,\n\t\tBaseURL:   baseURL,\n\t\tClient:    &http.Client{Timeout: 15 * time.Second},\n\t}\n}\n\nfunc (c *InfiniClient) signRequest(method, path string, body []byte) (map[string]string, error) {\n\tgmtTime := time.Now().UTC().Format(\"Mon, 02 Jan 2006 15:04:05 GMT\")\n\n\tsigningString := fmt.Sprintf(\n\t\t\"%s\\n%s %s\\ndate: %s\\n\",\n\t\tc.KeyID,\n\t\tstrings.ToUpper(method),\n\t\tpath,\n\t\tgmtTime,\n\t)\n\n\tmac := hmac.New(sha256.New, []byte(c.SecretKey))\n\tmac.Write([]byte(signingString))\n  \n\tsignature := base64.StdEncoding.EncodeToString(mac.Sum(nil))\n\n\tauthHeader := fmt.Sprintf(\n\t\t`Signature keyId=\"%s\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"%s\"`,\n\t\tc.KeyID, signature,\n\t)\n\n\theaders := map[string]string{\n\t\t\"Date\":          gmtTime,\n\t\t\"Authorization\": authHeader,\n\t}\n\n\t// 如果有 body，计算并添加 Digest header（不参与签名）\n\tif body != nil && len(body) > 0 {\n\t\tbodyDigest := sha256.Sum256(body)\n\t\tbodyDigestBase64 := base64.StdEncoding.EncodeToString(bodyDigest[:])\n\t\theaders[\"Digest\"] = fmt.Sprintf(\"SHA-256=%s\", bodyDigestBase64)\n\t}\n\n\treturn headers, nil\n}\n\nfunc (c *InfiniClient) Request(method, path string, payload interface{}) (map[string]interface{}, error) {\n\t// Encode JSON body if provided\n\tvar bodyBytes []byte\n\tif payload != nil {\n\t\tb, err := json.Marshal(payload)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbodyBytes = b\n\t}\n\n\t// Sign\n\theaders, err := c.signRequest(method, path, bodyBytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar body io.Reader\n\tif bodyBytes != nil {\n\t\tbody = bytes.NewBuffer(bodyBytes)\n\t\theaders[\"Content-Type\"] = \"application/json\"\n\t}\n\n\t// Build request\n\treq, err := http.NewRequest(method, c.BaseURL+path, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set headers\n\tfor k, v := range headers {\n\t\treq.Header.Set(k, v)\n\t}\n\n\t// Send\n\tresp, err := c.Client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\t// Check status\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\trespBodyBytes, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"request failed: %s, body=%s\", resp.Status, string(respBodyBytes))\n\t}\n\n\t// Decode JSON\n\tvar data map[string]interface{}\n\tif err := json.NewDecoder(resp.Body).Decode(&data); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n","lang":"go"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":5,"id":"java","__idx":11},"children":["Java"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","header":{"controls":{"copy":{}}},"source":"package com.infini.client;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.time.ZonedDateTime;\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Base64;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Locale;\n\n/**\n * Infini API 请求签名工具\n * 用于生成符合 Infini API 要求的认证 Headers\n */\npublic class RequestSigner {\n    private final String keyId;\n    private final String secretKey;\n\n    public RequestSigner(String keyId, String secretKey) {\n        this.keyId = keyId;\n        this.secretKey = secretKey;\n    }\n\n    /**\n     * 为 API 请求生成签名 Headers\n     * \n     * @param method HTTP 方法（GET、POST 等）\n     * @param path 请求路径（如 /v1/acquiring/order）\n     * @param body 请求体（JSON 字符串，如果是 GET 请求则传 null）\n     * @return 包含 Date、Authorization 和 Digest（如有 body）的 Headers Map\n     */\n    public Map<String, String> signRequest(String method, String path, String body) throws Exception {\n        // 生成 GMT 时间\n        ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);\n        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"EEE, dd MMM yyyy HH:mm:ss 'GMT'\", Locale.ENGLISH);\n        String gmtTime = now.format(formatter);\n\n        // 构建签名字符串（不包含 digest）\n        StringBuilder signingString = new StringBuilder();\n        signingString.append(keyId).append(\"\\n\");\n        signingString.append(method).append(\" \").append(path).append(\"\\n\");\n        signingString.append(\"date: \").append(gmtTime).append(\"\\n\");\n\n        // 计算 HMAC-SHA256 签名\n        Mac hmac = Mac.getInstance(\"HmacSHA256\");\n        SecretKeySpec secretKeySpec = new SecretKeySpec(\n            secretKey.getBytes(StandardCharsets.UTF_8), \n            \"HmacSHA256\"\n        );\n        hmac.init(secretKeySpec);\n        byte[] signatureBytes = hmac.doFinal(signingString.toString().getBytes(StandardCharsets.UTF_8));\n        String signature = Base64.getEncoder().encodeToString(signatureBytes);\n\n        // 构建 headers\n        Map<String, String> headers = new HashMap<>();\n        headers.put(\"Date\", gmtTime);\n        headers.put(\"Authorization\", \n            String.format(\"Signature keyId=\\\"%s\\\",algorithm=\\\"hmac-sha256\\\",headers=\\\"@request-target date\\\",signature=\\\"%s\\\"\",\n                keyId, signature)\n        );\n\n        // 如果有 body，计算并添加 Digest header（不参与签名）\n        if (body != null && !body.isEmpty()) {\n            MessageDigest digest = MessageDigest.getInstance(\"SHA-256\");\n            byte[] bodyDigest = digest.digest(body.getBytes(StandardCharsets.UTF_8));\n            String bodyDigestBase64 = Base64.getEncoder().encodeToString(bodyDigest);\n            headers.put(\"Digest\", \"SHA-256=\" + bodyDigestBase64);\n        }\n\n        return headers;\n    }\n\n    // 使用示例\n    public static void main(String[] args) {\n        try {\n            RequestSigner signer = new RequestSigner(\n                \"YOUR_KEY_ID\", \n                \"YOUR_SECRET_KEY\"\n            );\n\n            // GET 请求（无 body）\n            System.out.println(\"=== GET Request ===\");\n            Map<String, String> headers1 = signer.signRequest(\"GET\", \"/v1/acquiring/order?order_id=xxx\", null);\n            headers1.forEach((k, v) -> System.out.println(k + \": \" + v));\n\n            System.out.println(\"\\n=== POST Request ===\");\n            // POST 请求（有 body）\n            String jsonBody = \"{\\\"amount\\\":\\\"1.00\\\",\\\"currency\\\":\\\"USD\\\",\\\"request_id\\\":\\\"req-001\\\",\\\"client_reference\\\":\\\"client-001\\\",\\\"order_desc\\\":\\\"Test Order\\\",\\\"pay_methods\\\":[1,2]}\";\n            Map<String, String> headers2 = signer.signRequest(\"POST\", \"/v1/acquiring/order\", jsonBody);\n            headers2.forEach((k, v) -> System.out.println(k + \": \" + v));\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n}\n","lang":"java"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"45-时间偏差要求clock-skew","__idx":12},"children":["4.5 时间偏差要求（Clock Skew）"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["请求头中的 Date 必须与服务器时间保持 ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["±300 秒以内误差"]},"；否则将返回："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"401 Unauthorized\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["请确保服务器与 NTP 同步。"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"46-webhook-签名验证webhook-verification","__idx":13},"children":["4.6 Webhook 签名验证（Webhook Verification）"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Infini 向商户推送订单状态回调时，会附带签名。"," ","商户需对签名进行校验，以确认消息来源可信并防止内容被篡改。"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Webhook 请求包含以下 Header："]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Header"},"children":["Header"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"说明"},"children":["说明"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["X-Webhook-Timestamp"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Unix 时间戳"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["X-Webhook-Event-Id"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["本次事件唯一 ID"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["X-Webhook-Signature"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["HMAC-SHA256 签名值"]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"461-webhook-签名内容格式signing-content","__idx":14},"children":["4.6.1 Webhook 签名内容格式（Signing Content）"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["签名字符串格式："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"{timestamp}.{event_id}.{payload_body}\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["示例："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"1700000000.1234.{\"event\":\"order.completed\", \"order_id\":\"xxx\"}\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["计算方式："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"expected_signature = HMAC_SHA256(webhook_secret, signing_content)\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["判断合法性："]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"X-Webhook-Signature == expected_signature\n"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"462-webhook-验签示例python","__idx":15},"children":["4.6.2 Webhook 验签示例（Python）"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"@app.route('/webhook', methods=['POST'])\ndef handle_webhook():\n    signature = request.headers.get('X-Webhook-Signature')\n    timestamp = request.headers.get('X-Webhook-Timestamp')\n    event_id = request.headers.get('X-Webhook-Event-Id')\n\n    if not all([signature, timestamp, event_id]):\n        return {\"error\": \"Missing required headers\"}, 400\n\n    payload = request.get_data(as_text=True)\n    signed_content = f\"{timestamp}.{event_id}.{payload}\"\n\n    expected_sig = hmac.new(\n        WEBHOOK_SECRET.encode(),\n        signed_content.encode(),\n        hashlib.sha256\n    ).hexdigest()\n\n    if expected_sig != signature:\n        return {\"error\": \"Invalid signature\"}, 401\n\n    # Process webhook payload\n    return {\"status\": \"ok\"}\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"47-安全最佳实践","__idx":16},"children":["4.7 安全最佳实践"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["私钥（secret_key）仅展示一次，应立即安全备份。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["不得将 Secret Key 暴露在网页、JS、App 或公共仓库中。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["建议使用 KMS / Secret Manager 管理密钥。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["可在创建密钥时启用 IP 白名单限制访问来源。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Webhook 回调必须使用 HTTPS。"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["建议定期轮换密钥（Key Rotation）。"]}]}]},"headings":[{"value":"4. 授权与安全机制","id":"4-授权与安全机制","depth":2},{"value":"4.1 API 鉴权概述","id":"41-api-鉴权概述","depth":3},{"value":"4.2 签名字符串（Signing String）","id":"42-签名字符串signing-string","depth":3},{"value":"4.3 摘要和签名计算（Digest & Signature Calculation）","id":"43-摘要和签名计算digest--signature-calculation","depth":3},{"value":"Body Digest 计算（有 Body 时）","id":"body-digest-计算有-body-时","depth":4},{"value":"Signature 计算（所有请求）","id":"signature-计算所有请求","depth":4},{"value":"4.4 HTTP Headers 格式","id":"44-http-headers-格式","depth":3},{"value":"通用客户端示例","id":"通用客户端示例","depth":4},{"value":"Python","id":"python","depth":5},{"value":"Nodejs","id":"nodejs","depth":5},{"value":"Golang","id":"golang","depth":5},{"value":"Java","id":"java","depth":5},{"value":"4.5 时间偏差要求（Clock Skew）","id":"45-时间偏差要求clock-skew","depth":3},{"value":"4.6 Webhook 签名验证（Webhook Verification）","id":"46-webhook-签名验证webhook-verification","depth":3},{"value":"4.6.1 Webhook 签名内容格式（Signing Content）","id":"461-webhook-签名内容格式signing-content","depth":4},{"value":"4.6.2 Webhook 验签示例（Python）","id":"462-webhook-验签示例python","depth":4},{"value":"4.7 安全最佳实践","id":"47-安全最佳实践","depth":3}],"frontmatter":{"seo":{"title":"4. 授权与安全机制"}},"lastModified":"2026-06-12T06:47:16.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/zh/docs/en/4-authorization","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}