基于 SIWX (CAIP-122) 的跨链统一身份认证
长久以来,Web3 身份认证标准几乎完全被 EIP-4361 (Sign-In with Ethereum / SIWE) 所主导。它确立了使用以太坊私钥签名替代传统邮箱和密码登录的标准密码学范式。
然而,现代生产级 DApp 极少局限于单一公链。随着去中心化应用扩展至高吞吐量的高性能非 EVM 运行时(如 Solana),工程团队必然陷入单链孤岛的架构泥潭中。
1. 架构死局:单链孤岛的反模式
┌────────────────────────────────────────────────────────┐│ Divergent Auth Spaghetti │├───────────────────────────┬────────────────────────────┤│ EVM Stack │ Solana Stack │├───────────────────────────┼────────────────────────────┤│ • EIP-4361 Message Format │ • Arbitrary raw text/JSON ││ • viem / ethers verify │ • tweetnacl / ed25519 hook ││ • Hex Address Encoding │ • Base58 Address Encoding ││ • Custom Session Cookie A │ • Custom Session Cookie B │└───────────────────────────┴────────────────────────────┘碎片化鉴权的核心安全隐患
- 鉴权边界穿透:后端接口无法原子化断言当前会话是否有权发起跨链调度或调用特定敏感 RPC。
- 跨链 Nonce 污染攻击:缺乏命名空间隔离的挑战码可能被重放或在不同链环境被恶意串用。
- 防御成本翻倍:两套独立的 Cookie 规范与 CSRF 防护层大幅放大了系统的攻击面。
2. 走向标准:CAIP-122 链无关身份模型
eip155:1:0x1234567890abcdef1234567890abcdef12345678 <-- Ethereum Mainnet (CAIP-10)solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:9WzDXwBbmkg... <-- Solana Mainnet (CAIP-10)认证生命周期流转
完整的端到端认证生命周期——从服务端签发单次 Challenge Nonce,到多链密码学验签以及原子化防重放会话创建:
3. SIWX 模块化架构:包结构与适配器
TUWA SIWX 采用彻底解耦的分层设计:@tuwaio/siwx-core 零依赖定义标准类型;@tuwaio/siwx-evm 与 @tuwaio/siwx-solana 负责各链密码学验签;@tuwaio/siwx-server 提供服务端会话防护。
┌────────────────────────────────────────────────────────────────────────┐│ TUWA SIWX Modular Architecture │├────────────────────────────────────────────────────────────────────────┤│ L1: Pure Core Engine ││ • @tuwaio/siwx-core CAIP-122 Builder & Parser (+orbit-core) │├────────────────────────────────────────────────────────────────────────┤│ L2: Network Adapters (Multi-Chain Primitives) ││ • @tuwaio/siwx-evm EVM: EIP-191 / EIP-1271 / ERC-6492 ││ • @tuwaio/siwx-solana Solana: @solana/kit / SubtleCrypto │├────────────────────────────────────────────────────────────────────────┤│ L2: Presentation & Bindings ││ • @tuwaio/siwx-react React Hooks & Zustand Session Store ││ • @tuwaio/siwx-server Verifier, Next.js Handlers, JWT + JWKS │└────────────────────────────────────────────────────────────────────────┘4. 跨链网络适配器对比分析
适配器统一实现 SiwxSigner 接口,使应用层代码完全脱离底层钱包连接库的 API 差异:
| 技术维度 | EVM 适配器 (@tuwaio/siwx-evm) | Solana 适配器 (@tuwaio/siwx-solana) |
|---|---|---|
| CAIP-2 Namespace | eip155:{chainId}(如 eip155:1) | solana:{genesisHash}(如 solana:5eykt4Us...) |
| 地址表达格式 | 20 字节 Hex(0x[a-fA-F0-9]{40}) | 32 字节 Base58([1-9A-HJ-NP-za-km-z]{32,44}) |
| CAIP-10 规范全名 | eip155:1:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B | solana:5eykt4Us...:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM |
| 密码学曲线 | secp256k1 (Koblitz ECDSA) | ed25519 (Edwards-curve Digital Signature) |
| 签名结果格式 | 65 字节 Hex 字符串(0x + 130 字符: r, s, v) | 64 字节 Base58 字符串(88 字符) |
| 受支持钱包生态 | MetaMask、Rabby、Rainbow、Coinbase、Safe 及所有 EIP-1193 钱包 | Phantom、Solflare、Backpack、OKX 及所有 Wallet Standard 钱包 |
| 智能合约账户 | EIP-1271(已部署)与 ERC-6492(未部署),在签名链验证 | 不支持(仅支持密钥对;PDA 位于曲线外无私钥) |
| 签名验证引擎 | Viem recoverAddress + verifyMessage (EIP-1271/6492) | 原生 Web Crypto API crypto.subtle.verify(Ed25519,零依赖) |
5. TUWA 生态端到端实施方案
步骤 1:客户端多链签名组件实现
使用 Satellite Connect 的编程式登录按钮
// src/components/SignInButton.tsx'use client'; import { createEvmSiwxSigner } from '@tuwaio/evm-sdk/siwx';import { useSatelliteConnectStore } from '@tuwaio/sdk/satellite';import { createSatelliteSiwxSigner, getSatelliteSiwxFields, isSessionMatchingConnection, useSiwx, useSiwxSession,} from '@tuwaio/sdk/siwx';import type { SolanaConnection } from '@tuwaio/solana-sdk/satellite'; import { wagmiConfig } from '@/configs/appConfig'; export function SignInButton() { const activeConnection = useSatelliteConnectStore((state) => state.activeConnection); const { signIn, signOut } = useSiwx(); const { session, status } = useSiwxSession(); if (!activeConnection?.isConnected) return null; // The SIWX helpers read the account and the chain of the connection const account = { address: activeConnection.address, chainId: activeConnection.chainId }; // The session was issued for this account and chain: an account or network switch shows "Sign in" again if (isSessionMatchingConnection(session, account)) { return ( <button onClick={async () => { await fetch('/api/siwx/logout', { method: 'POST' }); signOut(); }} > Sign out </button> ); } const handleSignIn = async () => signIn({ // EVM wallets sign through wagmi; a Solana connection carries the signMessage of its Wallet Standard wallet signer: activeConnection.connectorType.startsWith('evm:') ? createEvmSiwxSigner(wagmiConfig) : await createSatelliteSiwxSigner(activeConnection as SolanaConnection), getNonce: async () => { const res = await fetch('/api/siwx/nonce'); return ((await res.json()) as { nonce: string }).nonce; }, verifier: async (payload) => { const res = await fetch('/api/siwx/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return res.ok ? res.json() : null; }, // eip155:11155111:0x… for an EVM wallet, solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1:… for a Solana devnet one fields: getSatelliteSiwxFields(account, { statement: 'Sign in to access TUWA sovereign services.' }), onSuccess: (session) => console.log('[SIWX] Signed in:', session.address), }); return ( <button onClick={handleSignIn} disabled={status !== 'idle' && status !== 'error'}> Sign in </button> );}结合 Satellite 与 Nova UI Kit 的声明式 Watcher 模式
// src/providers/SatelliteConnectProviders.tsx'use client'; import { EVMConnectorsWatcher } from '@tuwaio/evm-sdk/nova-connect';import { satelliteEVMAdapter } from '@tuwaio/evm-sdk/satellite';import { NovaConnectProvider } from '@tuwaio/sdk/nova-connect';import { SatelliteConnectProvider } from '@tuwaio/sdk/nova-connect/satellite';import { SolanaConnectorsWatcher } from '@tuwaio/solana-sdk/nova-connect';import { satelliteSolanaAdapter } from '@tuwaio/solana-sdk/satellite'; import { appEVMChains, solanaRPCUrls, wagmiConfig } from '@/configs/appConfig'; export function SatelliteConnectProviders({ children }: { children: React.ReactNode }) { return ( <SatelliteConnectProvider adapter={[satelliteEVMAdapter(wagmiConfig, appEVMChains), satelliteSolanaAdapter({ rpcUrls: solanaRPCUrls })]} autoConnect={true} > {/* The watchers follow account and network changes made in the wallet */} <EVMConnectorsWatcher wagmiConfig={wagmiConfig} /> <SolanaConnectorsWatcher /> <NovaConnectProvider appChains={appEVMChains} solanaRPCUrls={solanaRPCUrls} siwx={{ expirationSeconds: 1800, getNonce: async () => { const res = await fetch('/api/siwx/nonce'); return ((await res.json()) as { nonce: string }).nonce; }, verifier: async (payload) => { const res = await fetch('/api/siwx/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return res.ok ? res.json() : null; }, destroyer: async () => { await fetch('/api/siwx/logout', { method: 'POST' }); }, }} > {children} </NovaConnectProvider> </SatelliteConnectProvider> );}步骤 2:服务端密码学签名校验路由
生产级 Route Handler(原子 Redis Nonce 与会话存储)
// src/app/api/siwx/[...siwx]/route.tsimport type { SiwxNonceStore, SiwxSession, SiwxSessionRecord, SiwxSessionStore } from '@tuwaio/sdk/siwx/server';import { createSiwxApiHandler } from '@tuwaio/sdk/siwx/server-next'; import { redisClient } from '@/lib/redis'; // Atomic Redis session store conforming to SiwxSessionStoreconst sessionStore: SiwxSessionStore = { async get(id: string): Promise<SiwxSessionRecord | null> { const data = await redisClient.get(`siwx:sess:${id}`); return data ? (JSON.parse(data) as SiwxSessionRecord) : null; }, async create({ session, ttlSeconds }: { session: SiwxSession; ttlSeconds: number }): Promise<SiwxSessionRecord> { const id = crypto.randomUUID(); const createdAt = Date.now(); const expiresAt = createdAt + ttlSeconds * 1000; const record: SiwxSessionRecord = { id, session, createdAt, expiresAt }; await redisClient.set(`siwx:sess:${id}`, JSON.stringify(record), 'EX', ttlSeconds); return record; }, // Links the wallet session to your own user record after sign-in; SIWX itself never calls it async bindSubject(id: string, subjectId: string): Promise<boolean> { const data = await redisClient.get(`siwx:sess:${id}`); if (!data) return false; const record = { ...(JSON.parse(data) as SiwxSessionRecord), subjectId }; await redisClient.set(`siwx:sess:${id}`, JSON.stringify(record), 'KEEPTTL'); return true; }, async revoke(id: string): Promise<void> { await redisClient.del(`siwx:sess:${id}`); },}; // Atomic Redis nonce store preventing replay attacksconst nonceStore: SiwxNonceStore = { async issue({ nonce, ttlSeconds }: { nonce: string; ttlSeconds: number }) { await redisClient.set(`siwx:nonce:${nonce}`, '1', 'EX', ttlSeconds); }, async consume({ nonce }: { nonce: string }) { const result = await redisClient.del(`siwx:nonce:${nonce}`); return result === 1; // Atomic consumption prevents double-spend/replay },}; const handler = createSiwxApiHandler({ sessionStore, nonceStore, policy: { expectedDomain: process.env.NEXT_PUBLIC_APP_DOMAIN!, requireExpirationTime: true, maxIssuedAtAgeSeconds: 300, // 5 minutes maxSessionLifetimeSeconds: 60 * 60 * 24 * 7, // 7 days clockSkewSeconds: 60, }, cookieOptions: { name: 'siwx-session', secure: process.env.NODE_ENV === 'production', },}); export const { GET, POST, DELETE } = handler;无基础设施 Stateless Route Handler(HMAC 签名令牌)
// src/app/api/siwx/[...siwx]/route.tsimport { createStatelessDemoSiwxHandler } from '@tuwaio/sdk/siwx/server-next'; const handler = createStatelessDemoSiwxHandler({ signingSecret: process.env.SIWX_SIGNING_SECRET!, // Minimum 32 characters policy: { expectedDomain: ['localhost:3000', process.env.NEXT_PUBLIC_APP_DOMAIN!], requireExpirationTime: true, maxSessionLifetimeSeconds: 1800, // 30 minutes }, cookieOptions: { name: 'siwx-demo-session', secure: process.env.NODE_ENV === 'production', },}); export const { GET, POST, DELETE } = handler;步骤 3:保护 Server Actions 与后端调度
// src/app/actions/transaction.ts'use server'; import { getSiwxServerSession, isSessionMatchingTarget } from '@tuwaio/sdk/siwx/server';import { cookies } from 'next/headers'; import { sessionStore } from '@/lib/authStores'; export async function submitProtectedOperation({ targetAddress, chainId, actionPayload,}: { targetAddress: string; chainId: string; actionPayload: Record<string, unknown>;}) { const cookieStore = await cookies(); // 1. Resolve active session from durable store or signed cookie const session = await getSiwxServerSession({ cookieSource: cookieStore, cookieName: 'siwx-session', sessionStore, // Or `signingSecret` when using stateless profile policy: { expectedDomain: process.env.NEXT_PUBLIC_APP_DOMAIN!, }, }); if (!session) { throw new Error('Unauthorized: No active or valid SIWX session found.'); } // 2. Cryptographic claim verification: assert wallet ownership matches target const isMatch = isSessionMatchingTarget(session, targetAddress, chainId); if (!isMatch) { throw new Error( `Forbidden: Session wallet (${session.address}) does not match transaction sender (${targetAddress}).`, ); } // 3. Proceed with authorized operation console.log(`[Authorized] Dispatched for ${session.address} on ${session.chainId}`); return { success: true, processedAt: new Date().toISOString() };}6. 方案技术对比:EIP-4361 vs CAIP-122 (SIWX)
| 架构维度 | EIP-4361 (SIWE) | CAIP-122 (@tuwaio/siwx) |
|---|---|---|
| 支持链范围 | 仅以太坊及 EVM 兼容链 | 全多链 (EVM、Solana,CAIP-2 可扩展) |
| 地址格式 | 纯 Hex 格式 (0x...) | 全格式支持:Hex 与 Base58 (CAIP-10) |
| 重放防御 | 依赖单链服务端字符串比对 | 原子化单次 Nonce 生命周期模型 |
总结
基于 CAIP-122 与 TUWA SIWX,工程团队能够以纯粹非托管的方式统一 EVM 与 Solana 的用户身份认证。
📚 后续指引
- @tuwaio/siwx 空间 :查看完整 API 规范与类型定义。
- 全栈 React 应用搭建:端到端整合 Nova 与 Pulsar。
最后更新于