SIWX 跨系统鉴权:通过 JWT 与 JWKS 对接外部服务
SIWX 会话默认以高安全性 HttpOnly Cookie 存储在服务端,仅限您的自有后端读取。但在现代 Web3 架构中,许多外部系统(例如支持自定义认证的嵌入式钱包服务商 Coinbase CDP 、第三方微服务及无状态网关)仅接收基于非对称公钥验签的 JWT 凭据。
本指南将统一两者:终端用户只需使用私钥对一条 CAIP-122 消息签名一次,全链路各系统即可获得可信身份:
GET /api/siwx/token:为已验证的 SIWX 会话即时下发短时效非对称 JWT;GET /api/siwx/jwks:公开标准 JWKS 密钥集,供第三方验签拉取;- 无缝集成 Coinbase CDP Custom Authentication,为登录用户自动挂载嵌入式智能钱包;
- 在任意自建独立微服务中,零外部数据库依赖即可本地秒级验签。
路由配置步骤提供 EVM 与 Solana 切换选项卡,已与其他指南保持同步。
架构流转与验签机制
Wallet ──signs CAIP-122──▶ POST /api/siwx/verify ──▶ session cookieBrowser ──cookie──▶ GET /api/siwx/token ──▶ JWT (ES256, 10 minutes)Provider ──▶ GET /api/siwx/jwks ──▶ public keys ──▶ checks the JWT signature, iss, aud, expProvider ──▶ finds or creates the user of `sub`开始之前
- 全栈 React 指南中的应用,包含 SIWX 路由及
src/lib/authStores.ts。 - 公开的 HTTPS 域名,供第三方拉取 JWKS(参见本地开发章节)。
步骤 1:生成 JWT 签名密钥对
在本地或构建容器中执行一次性密钥生成脚本,默认生成高强度、短签名的 ES256 (P-256) 椭圆曲线密钥:
scripts/generate-siwx-jwt-key.mjs
import { generateSiwxJwtKey } from '@tuwaio/sdk/siwx/server'; const { privateJwk, kid } = await generateSiwxJwtKey('ES256');console.log(`kid: ${kid}`);console.log(`SIWX_JWT_PRIVATE_KEY='${JSON.stringify(privateJwk)}'`);node scripts/generate-siwx-jwt-key.mjs将输出的 SIWX_JWT_PRIVATE_KEY 妥善写入环境变量(.env.local 及云端生产环境变量)。
步骤 2:在 API 路由处理器中启用 JWT 下发
配置 jwt 参数后,API 处理器将自动开启 /token 与 /jwks 路由:
src/app/api/siwx/[...siwx]/route.ts
import type { EvmVerifyClient } from '@tuwaio/evm-sdk/siwx';import { importSiwxJwtKey } from '@tuwaio/sdk/siwx/server';import { createSiwxApiHandler } from '@tuwaio/sdk/siwx/server-next';import { createPublicClient, http } from 'viem'; import { appChains } from '@/configs/appConfig';import { nonceStore, sessionStore } from '@/lib/authStores'; const appUrl = new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'); const clients = new Map<number, EvmVerifyClient>( appChains.map((chain) => [chain.id, createPublicClient({ chain, transport: http() })]),); export const { GET, POST, DELETE } = createSiwxApiHandler({ sessionStore, nonceStore, policy: { expectedDomain: appUrl.host, expectedUri: appUrl.origin, requireExpirationTime: true, maxIssuedAtAgeSeconds: 300, }, verifyOptions: { publicClient: (chainId) => clients.get(chainId) }, jwt: { signingKey: importSiwxJwtKey({ privateKey: process.env.SIWX_JWT_PRIVATE_KEY ?? '' }), issuer: appUrl.origin, audience: 'my-app', },});GET /api/siwx/token返回当前会话签发的短时效 JWT(默认有效期 10 分钟),响应头带Cache-Control: no-store;无会话则直接返回 401。GET /api/siwx/jwks返回公钥集并自带 5 分钟公共 CDN 缓存策略。
步骤 3:配置 Coinbase CDP 自定义认证
登录 CDP 开发者后台 ,在项目的 Custom Authentication 设置中填入参数:
| 配置项 | 参数值 |
|---|---|
| JWKS endpoint URL | https://<您的应用域名>/api/siwx/jwks |
| Issuer (iss) | 服务端声明的发行者,如 https://<您的应用域名> |
| Audience (aud) | 声明的受信受众 (my-app) |
| User identifier claim | 保持留空:CDP 将默认映射标准 sub 声明作为唯一用户标识 |
编写前端动态获取 JWT 的工具函数:
src/lib/siwxJwt.ts
export async function getSiwxJwt(): Promise<string | undefined> { const res = await fetch('/api/siwx/token', { cache: 'no-store' }); if (!res.ok) return undefined; const { token } = (await res.json()) as { token: string; expiresAt: number }; return token;}在 React 树顶层包裹 CDP Provider:
src/providers/CdpProvider.tsx
'use client'; import { CDPHooksProvider, useAuthenticateWithJWT, useIsSignedIn } from '@coinbase/cdp-hooks';import { useSiwxSession } from '@tuwaio/sdk/siwx';import { type ReactNode, useEffect } from 'react'; import { getSiwxJwt } from '@/lib/siwxJwt'; function CdpSignIn() { const { isAuthenticated } = useSiwxSession(); const { isSignedIn } = useIsSignedIn(); const { authenticateWithJWT } = useAuthenticateWithJWT(); useEffect(() => { if (isAuthenticated && !isSignedIn) void authenticateWithJWT(); }, [isAuthenticated, isSignedIn, authenticateWithJWT]); return null;} export function CdpProvider({ children }: { children: ReactNode }) { return ( <CDPHooksProvider config={{ projectId: process.env.NEXT_PUBLIC_CDP_PROJECT_ID ?? '', customAuth: { getJwt: getSiwxJwt }, ethereum: { createOnLogin: 'eoa' }, }} > <CdpSignIn /> {children} </CDPHooksProvider> );}步骤 4:在独立微服务中本地验签
任何能够通过网络拉取 JWKS 的后端服务均可通过内置函数快速解析并校验用户身份:
services/orders/src/auth.ts
import { type SiwxJwks, verifySiwxJwt } from '@tuwaio/sdk/siwx/server'; const APP_URL = 'https://app.example.com'; export async function walletOf(request: Request): Promise<string | null> { const token = request.headers.get('authorization')?.replace(/^Bearer /, ''); if (!token) return null; const jwks = (await (await fetch(`${APP_URL}/api/siwx/jwks`)).json()) as SiwxJwks; const claims = await verifySiwxJwt(token, { jwks, issuer: APP_URL, audience: 'my-app' }); return claims?.sub ?? null;}JWT 载荷声明明细 (Claims)
| 声明 (Claim) | 内容说明 |
|---|---|
iss | 发行者应用 URL 域名标识 |
aud | 预期的接收方受众标识 |
sub | 用户唯一主体:EOA 钱包格式为 eip155:0x...,Solana 格式为 solana:<address> |
caip10 | CAIP-10 规范全限定账户标识(如 eip155:8453:0xAb58...) |
chain_id | 用户签名时的 CAIP-2 链标识 |
安全考量与密钥轮换
- 保持短时效: 默认 10 分钟已足够,切勿持久化存储在
localStorage。 - 密钥服务端隔离:
SIWX_JWT_PRIVATE_KEY仅作为部署私密凭证保留在服务器。 - 零停机轮换: 生成新密钥后移入
signingKey,旧公钥配置到jwt.previousKeys供自然过渡。
本地调试建议
第三方服务需拉取 JWKS 公钥,本地 localhost 无法直接访问。建议使用隧道(如 ngrok)或预览环境进行完整联调。
📚 下一步探索
最后更新于