Skip to Content
API 参考身份验证

身份验证

InfraIO Pay 有两套 API 表面,鉴权模型不同。挑选与调用方匹配的那一套:

表面路径前缀受众鉴权
商户 B2B/b2b/v1/*你的服务器HMAC-SHA256 请求签名
仪表板按服务划分:/auth/*/payment/*/merchant/*/event/*/user/*商户仪表板的浏览器会话Bearer JWT

本页面介绍 B2B 表面 — 也就是你用 API 密钥对从服务端调用的那一套。 如果你在嵌入 InfraIO 仪表板或构建内部工具,请使用仪表板表面(文档单独 维护,尚未公开)。

gateway 按照前导前缀为每个表面路由,并在转发前 剥离 该前缀: /b2b/v1/checkout-sessions/quick 到达 payment-service 时是 /v1/checkout-sessions/quick,而仪表板的 /payment/v1/orders 到达时是 /v1/orders。所以如果你在别处看到光秃秃的 /v1/* 路径,那是公共前缀 被移除之后的 后端内部 路径 — 你的客户端始终发送带前缀的形式。(对 签名的一个影响:B2B canonical 字符串签名的路径 仍带着 /b2b 前缀 — 见下文。)

端点

环境Base URL
Testhttps://api-dev.infraio.xyz
Livehttps://api.infraio.xyz

URL 模式相同 — 环境由密钥前缀(pk_test_… vs pk_live_…)控制, 而不是 URL。

密钥对

你从商户仪表板(Developers → API keys → + Add key)获得两个值:

  • Publishable key (pk_test_…pk_live_…)— 标识你的账户。 作为 X-Client-ID 发送。可安全嵌入浏览器 bundle(SDK 已经在用)。
  • Secret key (sk_test_…sk_live_…)— HMAC 签名密钥。仅限 服务端。请像数据库密码一样对待。

如果 secret key 不慎进入浏览器 bundle、git 仓库、日志行,或被分享 到聊天 — 立刻从仪表板吊销。没有重叠窗口;吊销立即生效。请重新 签发并重新部署。

对请求签名

每次 /b2b/v1/* 调用都携带三个标头:

X-Client-ID: pk_live_70d0becbd7a53047b0dfe88a960d10ad X-Timestamp: 1715990400 X-Signature: 9a8b7c6d… (hex HMAC-SHA256)

签名在一个规范化字符串上计算:

METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODY
  • METHOD — 大写的 HTTP 动词(POSTGET、……)。
  • PATH — 请求路径包含 /b2b 前缀,不含 host 且不含查询字符串 (例如 /b2b/v1/checkout-sessions/quick)。Gateway 在剥离 /b2b 之前就用原始入站路径验证签名,所以前缀必须在。查询参数参与签名 — 对于 GET …?cursor=…&limit=20,只对路径签名,不要包含 ?… 部分。
  • TIMESTAMP — Unix 秒,十进制字符串(例如 "1715990400"),与 X-Timestamp 完全一致。
  • BODY — 原始请求 body 字节。GET/DELETE 为空字符串。

secret key 作为密钥的 HMAC-SHA256 签名,输出 hex:

import { createHmac } from "node:crypto"; function sign({ method, path, body, secret }: { method: string; path: string; body: string; secret: string; }) { const timestamp = Math.floor(Date.now() / 1000).toString(); const input = [method.toUpperCase(), path, timestamp, body].join("\n"); const signature = createHmac("sha256", secret).update(input).digest("hex"); return { timestamp, signature }; }

为什么选 HMAC,而不是 Bearer?

裸 Bearer-token API 会在每次请求里把你唯一的 secret 经线传输。任何 拿到某个 TLS 终止代理日志的人都能拿到你账户的钥匙。HMAC 签名意味着 secret 永远不上路 — 上路的只是其衍生签名,而且一次性(绑定到该次 请求 + 该分钟)。

代价是:每次调用都得算签名。服务端 SDK 会隐藏这部分;在我们发布之前, 上面的 helper 大约每种语言 15 行。

时间戳容差

绑定容差为 ±5 分钟(300 秒),由 merchant-service 在校验签名时 强制执行。Gateway 自身略宽松一些(310s)作为纵深防御,但通过 gateway 却在内层检查失败的请求仍以 401 invalid_signature 结束 — 把契约当作 300s。两点含义:

  1. 同步服务器时钟到 NTP。时钟漂移的长跑 cron 会间歇失败。
  2. 不要预先计算并入队签名。 如果请求在重试队列里坐了 > 5 分钟, 它的签名就过期了。

密钥 scope

Secret key 携带一个或多个下面的 scope 包:

Scope预期用途
read列出/读取订单、会话、退款
write_order创建结账会话、订单
write_refund发起退款、铸造退款申请 token
webhook_manage创建/更新/删除 Webhook 端点

仪表板默认签发”全权限”密钥(全部四个 scope)。你可以从 Developers → API keys → + Add key 铸造受限 scope 的密钥,仅勾选 集成需要的 scope。

scope 强制目前为建议性,并未实际拦截。 scope 会记录在密钥上, 并在仪表板回显给你,但 gateway 中间件尚未拒绝超出 scope 的调用 — 任何有效的 sk_… 密钥今天表现为全权限。按端点的 scope 闸门将在 下一个版本上线。在此之前请勿把 scope 当作安全边界;先把它当作标签, 通过轮换 / 吊销密钥来限制访问。

签名在哪里被验证

HMAC 校验只在 gateway 进行一次。Gateway 的步骤:

  1. 读取 X-Client-IDX-TimestampX-Signature
  2. pk_… 查找商户 + secret,运行时间戳窗口检查,重新计算签名, 常量时间比较。
  3. 成功后,剥离鉴权标头,给请求打上内部标头(X-B2B-Auth: 1X-Merchant-IDX-Merchant-Domain),然后转发到下游服务 (payment-service、merchant-service 等)。环境与解析后的 scope 今天注入到标头里 — 下游需要环境时,从请求 body / 每商户 配置推导,而不是从标头。
  4. 失败则返回 401 INVALID_SIGNATURE,完全不触达后端。

下游服务重新跑 HMAC — 它们信任 gateway 注入的标头,并对 gateway 解析出的商户进行操作。它们也按端点执行 scope 闸门:如上所述, 密钥的 scope 不会被注入,因此任何通过认证的 sk_… 都能触达其商户的 任意端点(scope 强制目前只是建议性的 — 见 密钥 scope 下的提示框)。这有两个影响:

  • 如果你在 InfraIO Pay 前面运营自己的反向代理,请勿剥离 X-B2B-Auth / X-Merchant-ID(也不要伪造它们 — gateway 会拒绝 公网入口携带这些标头的请求)。
  • 公网路径(/b2b/v1/*)是唯一跑 HMAC 步骤的表面。我们服务之间的 内部 gRPC 使用 mTLS — 一种不接受 X-Client-ID 的不同信任模型。

下一步

  • 错误 — 4xx/5xx 的响应形态。
  • 安全 → API 密钥 — 轮换、吊销,以及 secret 泄露时的处理。
  • Webhooks → 签名验证 — 使用不同的 HMAC 方案(标头 X-Signature: sha256=…,签名 X-Timestamp + "." + raw_body,加上 24 小时轮换宽限窗口内可选的 X-Signature-Prev)。不要把两种方案搞混 — 它们共用哈希算法, 但签名字节和 secret 家族(whsec_… vs sk_…)不同。