x402 sign-in-with-x 扩展:基于 CAIP-122 的钱包认证规范与 SDK 实现详解
【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402
本文以 sign-in-with-x 扩展规范 为主体,完整讲解 x402 协议中sign-in-with-x(SIWX)扩展的报文结构、字段语义、验证逻辑与安全模型,并结合 x402 TypeScript SDK 的扩展包源码 说明 nonce 生成、消息校验、签名验证和钩子(Hooks)的真实实现方式。读完本文,你可以独立实现一个支持"已支付用户免重复付款"和"纯认证路由"的 x402 服务端,并理解 402 响应到SIGN-IN-WITH-X请求头的完整链路。
1. SIWX 定位:Server ↔ Client 的钱包认证扩展
sign-in-with-x扩展实现了 CAIP-122 规范的钱包认证能力:客户端通过签名一个由服务端下发的挑战消息(challenge message),证明其控制某个钱包地址。它的核心价值有两个:
- 已购内容重复访问免支付:客户端证明"这个钱包曾经为该资源付过款",服务端核验后放行,不再要求重复付款;
- 纯认证路由(auth-only):路由只要求钱包签名、不要求任何支付。
需要特别强调的是,规范明确指出:这是一个 Server ↔ Client 扩展,Facilitator 不参与认证流程。也就是说,认证链路上的挑战下发、签名验证全部发生在服务端与客户端之间,支付结算方(Facilitator)只负责此前的付款结算环节。
整体交互流程为四步:
- 客户端访问受保护资源,服务端返回
402 Payment Required,其中extensions对象携带sign-in-with-x挑战参数; - 客户端用自己的钱包对 CAIP-122 消息签名;
- 客户端把签名证明以
SIGN-IN-WITH-XHTTP 请求头(base64 编码的 JSON)重新发送; - 服务端验证签名后,根据"路由是纯认证型"或"该钱包已为此资源付过款"两种条件之一授予访问权。
2. 服务端声明:402 Payment Required 中的扩展结构
服务端通过在402 Payment Required响应的extensions对象中包含sign-in-with-x键来宣告 SIWX 支持。规范给出的完整报文示例如下(EVM 单链场景,Base 测试网 USDC):
{ "x402Version": "2", "accepts": [ { "scheme": "exact", "network": "eip155:8453", "amount": "10000", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", "maxTimeoutSeconds": 60, "extra": { "name": "USDC", "version": "2" } } ], "extensions": { "sign-in-with-x": { "info": { "domain": "api.example.com", "uri": "https://api.example.com/premium-data", "version": "1", "nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890", "issuedAt": "2024-01-15T10:30:00.000Z", "expirationTime": "2024-01-15T10:35:00.000Z", "statement": "Sign in to access premium data", "resources": ["https://api.example.com/premium-data"] }, "supportedChains": [ { "chainId": "eip155:8453", "type": "eip191" } ], "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "domain": { "type": "string" }, "address": { "type": "string" }, "statement": { "type": "string" }, "uri": { "type": "string", "format": "uri" }, "version": { "type": "string" }, "chainId": { "type": "string" }, "type": { "type": "string" }, "nonce": { "type": "string" }, "issuedAt": { "type": "string", "format": "date-time" }, "expirationTime": { "type": "string", "format": "date-time" }, "notBefore": { "type": "string", "format": "date-time" }, "requestId": { "type": "string" }, "resources": { "type": "array", "items": { "type": "string", "format": "uri" } }, "signature": { "type": "string" } }, "required": [ "domain", "address", "uri", "version", "chainId", "type", "nonce", "issuedAt", "signature" ] } } } }扩展对象由三部分组成:info(消息元数据)、supportedChains(认证方法声明)、schema(客户端证明的 JSON Schema)。
2.1 消息元数据info字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
domain | string | 必填 | 服务端域名(如"api.example.com"),必须与请求 host 匹配 |
uri | string | 必填 | 正在访问的完整资源 URI |
version | string | 必填 | CAIP-122 版本号,恒为"1" |
nonce | string | 必填 | 密码学随机数(32 位十六进制字符),服务端必须生成 |
issuedAt | string | 必填 | 挑战创建的 ISO 8601 时间戳 |
statement | string | 可选 | 人类可读的签名用途说明 |
expirationTime | string | 可选 | 挑战过期的 ISO 8601 时间戳,默认从issuedAt起 5 分钟 |
notBefore | string | 可选 | 签名生效之前的 ISO 8601 时间戳 |
requestId | string | 可选 | 请求关联 ID |
resources | string[] | 可选 | 与请求关联的 URI 列表 |
2.2 认证方法supportedChains[]字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
chainId | string | 必填 | CAIP-2 链标识,如"eip155:8453"、"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" |
type | string | 必填 | 签名算法:EVM 为"eip191",Solana 为"ed25519" |
signatureScheme | string | 可选 | 客户端签名 UX 提示:"eip191"、"eip1271"、"eip6492"或"siws" |
客户端选择supportedChains中与自身钱包匹配的第一个条目。
2.3 多链支持
同时支持 EVM 与 Solana 的服务端,可以在supportedChains中放入多条记录:
{ "x402Version": "2", "accepts": [...], "extensions": { "sign-in-with-x": { "info": { "domain": "api.example.com", "uri": "https://api.example.com/premium-data", "version": "1", "nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890", "issuedAt": "2024-01-15T10:30:00.000Z", "expirationTime": "2024-01-15T10:35:00.000Z", "statement": "Sign in to access premium data", "resources": ["https://api.example.com/premium-data"] }, "supportedChains": [ { "chainId": "eip155:8453", "type": "eip191" }, { "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "type": "ed25519" } ], "schema": {...} } } }关键点在于:同一条nonce被所有链共享。规范明确这样设计是为了"防止用不同钱包认证时发生重放攻击"——一次挑战只能兑换一次签名证明,无论客户端最终用哪条链的钱包去签。
3. 客户端证明:SIGN-IN-WITH-X请求头
客户端对挑战消息签名后,将证明以 base64 编码的 JSON 放入SIGN-IN-WITH-XHTTP 请求头:
GET /premium-data HTTP/1.1 Host: api.example.com SIGN-IN-WITH-X: eyJkb21haW4iOiJhcGkuZXhhbXBsZS5jb20iLCJhZGRyZXNzIjoiMHg4NTdiMDY1MTlFOTFlM0E1NDUzODc5MWJEYmIwRTIyMzczZTM2YjY2IiwidXJpIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vcHJlbWl1bS1kYXRhIiwidmVyc2lvbiI6IjEiLCJjaGFpbklkIjoiZWlwMTU1Ojg0NTMiLCJ0eXBlIjoiZWlwMTkxIiwibm9uY2UiOiJhMWIyYzNkNGU1ZjY3ODkwYTFiMmMzZDRlNWY2Nzg5MCIsImlzc3VlZEF0IjoiMjAyNC0wMS0xNVQxMDozMDowMC4wMDBaIiwiZXhwaXJhdGlvblRpbWUiOiIyMDI0LTAxLTE1VDEwOjM1OjAwLjAwMFoiLCJzdGF0ZW1lbnQiOiJTaWduIGluIHRvIGFjY2VzcyBwcmVtaXVtIGRhdGEiLCJyZXNvdXJjZXMiOlsiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vcHJlbWl1bS1kYXRhIl0sInNpZ25hdHVyZVNjaGVtZSI6ImVpcDE5MSIsInNpZ25hdHVyZSI6IjB4MmQ2YTc1ODhkNmFjY2E1MDVjYmYwZDlhNGEyMjdlMGM1MmM2YzM0MDA4YzhlODk4NmExMjgzMjU5NzY0MTczNjA4YTJjZTY0OTY2NDJlMzc3ZDZkYThkYmJmNTgzNmU5YmQxNTA5MmY5ZWNhYjA1ZGVkM2Q2MjkzYWYxNDhiNTcxYyJ9该请求头 base64 解码后得到如下证明载荷(客户端回显了服务端的所有info字段,并追加address、signature等字段):
{ "domain": "api.example.com", "address": "0x857b06519E91e3A54538791bDbb0E22373e36b66", "uri": "https://api.example.com/premium-data", "version": "1", "chainId": "eip155:8453", "type": "eip191", "nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890", "issuedAt": "2024-01-15T10:30:00.000Z", "expirationTime": "2024-01-15T10:35:00.000Z", "statement": "Sign in to access premium data", "resources": ["https://api.example.com/premium-data"], "signatureScheme": "eip191", "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c" }客户端在回显服务端字段之外,需要新增两个必填字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
address | string | 必填 | 签名钱包地址,EVM 使用 Checksum 格式,Solana 使用 Base58 |
signature | string | 必填 | 密码学签名,EVM 为0x...十六进制,Solana 为 Base58 |
SDK 侧用 Zod 对该载荷做了机器可校验的约束(见 SIWxPayloadSchema):type只允许eip191/ed25519,可选的signatureScheme只允许eip191/eip1271/eip6492/siws,这解释了示例载荷中signatureScheme字段的合法取值来源。
4. 支持的链与消息格式
4.1 EVM(eip155:*)
- Type:
eip191 - 签名方案:
eip191(EOA)、eip1271(智能合约钱包)、eip6492(无预部署/反事实钱包) - 消息格式:EIP-4361(SIWE,Sign-In With Ethereum)
- 链 ID 示例:
eip155:1(Ethereum)、eip155:8453(Base)、eip155:137(Polygon)
对应消息文本为:
api.example.com wants you to sign in with your Ethereum account: 0x857b06519E91e3A54538791bDbb0E22373e36b66 Sign in to access premium data URI: https://api.example.com/premium-data Version: 1 Chain ID: 8453 Nonce: a1b2c3d4e5f67890a1b2c3d4e5f67890 Issued At: 2024-01-15T10:30:00.000Z Expiration Time: 2024-01-15T10:35:00.000Z Resources: - https://api.example.com/premium-data4.2 Solana(solana:*)
- Type:
ed25519 - 签名方案:
siws - 消息格式:Sign-In With Solana(SIWS)
- 链 ID 示例:
solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp(mainnet)、solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1(devnet)
api.example.com wants you to sign in with your Solana account: BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW Sign in to access premium data URI: https://api.example.com/premium-data Version: 1 Chain ID: 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp Nonce: a1b2c3d4e5f67890a1b2c3d4e5f67890 Issued At: 2024-01-15T10:30:00.000Z Expiration Time: 2024-01-15T10:35:00.000Z Resources: - https://api.example.com/premium-dataSDK 中两种消息格式由 createSIWxMessage 按chainId前缀(eip155:/solana:)统一路由,遇到未知命名空间会直接抛出Unsupported chain namespace错误,这与服务端验证侧的路由策略保持对称。
5. 服务端验证逻辑
按规范,当服务端收到携带SIGN-IN-WITH-X请求头的请求时,应执行四步验证:
第 1 步:解析请求头。Base64 解码头值,再 JSON 解析得到证明载荷。
第 2 步:校验消息字段。
- Domain:
domain必须与请求 host 精确匹配; - URI:
uri必须以预期资源来源(origin)开头; - Issued At:必须足够新鲜(默认 < 5 分钟),且不得晚于当前时间(不得位于未来);
- Expiration:若存在
expirationTime,必须位于未来; - Not Before:若存在
notBefore,必须位于过去; - Nonce:必须唯一。服务端应当跟踪已用 nonce 以防重放攻击。
第 3 步:验证签名。按chainId前缀路由:
eip155:*:重建 SIWE 消息,用 ECDSA 恢复(EOA)或链上验证(EIP-1271 / EIP-6492 智能钱包);solana:*:重建 SIWS 消息,验证 Ed25519 签名。
第 4 步:检查支付历史。若签名有效,服务端检查恢复出的address是否曾为该资源付过款。规范明确这是应用层自定义逻辑,不属于协议本身。
SDK 对第 2、3 步的实现在以下文件中,可以逐条对照规范核对:
- validate.ts:
DEFAULT_MAX_AGE_MS常量固定为 5 分钟(第 11 行);domain 比对使用资源 URI 的hostname(遵循 EIP-4361 约定,不含端口,第 52-59 行);URI 校验允许消息中的uri是资源 origin 或完整 URL(第 61-68 行);nonce 唯一性通过可选的checkNonce回调交给应用层实现。 - verify.ts:
verifySIWxSignature按chainId前缀路由到 EVM 或 Solana 验证器。Solana 侧在验证前做了两道长度检查——Ed25519 签名必须为 64 字节、公钥必须为 32 字节(第 166-180 行),签名与地址均从 Base58 解码。EVM 侧默认只做本地 ECDSA 恢复;传入evmVerifier(兼容 viem 的publicClient.verifyMessage)后才启用 EIP-1271 / EIP-6492 智能钱包验证,这与规范"智能钱包验证需要 RPC 调用"的描述一致。
一个容易忽略的细节:从 types.ts 的注释 和 verify.ts 的路由实现 可以确认,决定验证算法的是chainId前缀而非type/signatureScheme字段——后两者对客户端只是选择签名 UX 的提示(hint)。这意味着服务端不能仅凭声明字段就信任签名的算法类型,必须按链命名空间实际路由,SDK 正是这样实现的。
6. 安全设计要点
规范列出了五点安全考量,均已在 SDK 中落地:
- 域名绑定(Domain Binding):
domain字段防止签名在不同服务之间被复用。SDK 中validateSIWxMessage对不匹配直接返回valid: false; - Nonce 唯一性:每个挑战必须有唯一 nonce 防重放。SDK 服务端扩展用
crypto.getRandomValues生成 16 字节随机数并格式化为 32 位十六进制(server.ts 第 64-66 行),恰好满足规范"32 hex characters"的要求; - 时间边界(Temporal Bounds):
issuedAt/expirationTime/notBefore三个字段共同约束签名的有效窗口; - 链特异性验证:签名按链适用算法验证,防止跨链签名复用;
- 智能钱包支持:EIP-1271 / EIP-6492 验证需要对钱包合约发起 RPC 调用,EOA 验证则完全本地完成。
7. SDK 实现深度解析:从声明到放行的完整链路
7.1 服务端声明:siwxResourceServerExtension的自动派生
规范中的info字段大多可以从请求上下文自动推导。SDK 的 siwxResourceServerExtension 实现了enrichPaymentRequiredResponse钩子,每次构造 402 响应时执行:
resourceUri缺省时取自请求 URL;domain缺省时从resourceUri的URL.hostname解析;networks缺省时从accepts[](即context.requirements)中提取去重后的network列表(第 55-61 行),并据此生成supportedChains;nonce/issuedAt每请求刷新;expirationTime仅在配置了expirationSeconds时才写入(不配置则载荷中无该字段,第 69-74 行);schema由buildSIWxSchema()生成,即规范示例中的 JSON Schema。
注意一个约束:对于纯认证路由(accepts: []),网络无法从支付要求推导,因此必须显式传入network参数。这一点在 docs/extensions/sign-in-with-x.mdx 的 API 说明中有专门提示,也与 DeclareSIWxOptions 类型定义 中network的注释一致。
7.2 支付记录:createSIWxSettleHook
"已支付地址"的追踪依赖结算钩子。createSIWxSettleHook 挂在x402ResourceServer.onAfterSettle()上,只在结算成功(ctx.result.success为真)时,从 Facilitator 结算结果中取出payer地址,并把资源 URL 归一化为路径后写入存储:
const storage = new InMemorySIWxStorage(); const resourceServer = new x402ResourceServer(facilitatorClient) .register(NETWORK, new ExactEvmScheme()) .registerExtension(siwxResourceServerExtension) // 每请求刷新 nonce/时间字段 .onAfterSettle(createSIWxSettleHook({ storage })); // 记录支付7.3 访问授予:createSIWxRequestHook的判定顺序
createSIWxRequestHook 挂在x402HTTPResourceServer.onProtectedRequest()上,处理顺序为:
- 从适配器读取
SIGN-IN-WITH-X请求头(大小写不敏感,两种形式都尝试),没有则直接放行给后续支付流程; parseSIWxHeader解析 →validateSIWxMessage校验字段;verifySIWxSignature验证签名并得到地址;- 若存储实现了 nonce 追踪,先检查
hasUsedNonce,命中则记录nonce_reused事件并拒绝(重放防护); - 判定放行条件:
routeConfig.accepts为空数组即视为纯认证路由(仅签名有效即放行);否则要求storage.hasPaid(path, address)为真(第 144-160 行); - 放行前把本次 nonce 记录为已用。
源码中还有一个健壮性细节:钩子创建时校验 nonce 追踪接口的完整性——hasUsedNonce和recordNonce必须同时实现或同时不实现,否则直接抛错(第 97-104 行),避免"检查了却记录不了"的半吊子状态。
7.4 支付历史存储:SIWxStorage接口
storage.ts 定义了最小接口与两个可选的 nonce 方法:
interface SIWxStorage { hasPaid(resource: string, address: string): boolean | Promise<boolean>; recordPayment(resource: string, address: string): void | Promise<void>; hasUsedNonce?(nonce: string): boolean | Promise<boolean>; // 可选:防重放 recordNonce?(nonce: string): void | Promise<void>; // 可选:防重放 }包内附带 InMemorySIWxStorage 供开发使用(地址统一转小写存储)。注释明确提示:生产多实例部署应自行实现持久化存储(数据库、Redis 等),且 nonce 记录应考虑过期清理以避免无限增长。
7.5 客户端侧:钩子与 fetch 包装
SDK 提供两种客户端接入方式:
方式一:createSIWxClientHook。挂在x402HTTPClient.onPaymentRequired()上,收到 402 后自动检查extensions["sign-in-with-x"],按签名者类型匹配supportedChains(Solana 签名者匹配ed25519,其余匹配eip191,见 hooks.ts 第 187-225 行),构造载荷、编码请求头并返回附加 header;失败时静默落回正常支付流程:
const httpClient = new x402HTTPClient(client) .onPaymentRequired(createSIWxClientHook(signer)); // 若服务端支持 SIWX,请求将自动先尝试认证、失败再付款 const response = await httpClient.fetch('https://api.example.com/data');方式二:wrapFetchWithSIWx。一个轻量 fetch 包装器(fetch.ts 第 42-100 行):对 402 响应解码PAYMENT-REQUIRED头,若含 SIWX 扩展则用accepts[0].network匹配链、签名并重试;若请求中已经带过SIGN-IN-WITH-X头则抛出异常防止无限循环。
手动实现同样可行,低层 API 组合为:declareSIWxExtension(构造声明)→parseSIWxHeader→validateSIWxMessage(payload, resourceUri, { maxAge?, checkNonce? })→verifySIWxSignature(payload, { evmVerifier? })→ 依据verification.address查支付历史放行。完整的分步示例见 docs/extensions/sign-in-with-x.mdx 的 Manual Usage 小节。
8. 端到端示例与延伸阅读
仓库提供了可直接运行的 TypeScript 端到端示例:
- 服务端示例:examples/typescript/servers/sign-in-with-x/index.ts(附 README);
- 客户端示例:examples/typescript/clients/sign-in-with-x/index.ts(附 README)。
其他可参考的仓库位置:
- 扩展规范原文:specs/extensions/sign-in-with-x.md,其"References"指向核心规范 specs/x402-specification-v2.md;
- 用户文档与快速上手:docs/extensions/sign-in-with-x.mdx(含 Smart Wallet 配置、多链路由示例、Troubleshooting 排障清单);
- SDK 扩展包全部 SIWX 源码:typescript/packages/extensions/src/sign-in-with-x/,测试:typescript/packages/extensions/test/sign-in-with-x.test.ts;
- 钩子机制的整体说明(
onAfterSettle/onProtectedRequest/onPaymentRequired等生命周期概念):docs/advanced-concepts/lifecycle-hooks.mdx; - Go SDK 的扩展文档也涉及该扩展:go/extensions/README.md。
9. 小结
sign-in-with-x是 x402 v2 中一个职责单一的 Server ↔ Client 扩展:服务端在 402 响应中用info+supportedChains下发 CAIP-122 挑战,客户端在SIGN-IN-WITH-X头中回传 base64 编码的签名证明,服务端按domain 绑定 → 时间窗口 → nonce 唯一性 → 链特异性验签 → 支付历史检查的顺序放行。规范定义了报文契约与安全边界,SDK(@x402/extensions包)则把 nonce 刷新、字段推导、验签路由、支付记录与纯认证路由判定全部封装为钩子与存储接口,开发者只需关注两件事:实现符合SIWxStorage语义的持久化存储,以及按业务需要决定是否启用 EIP-1271/EIP-6492 智能钱包验证。
【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考