身份认证 (Authentication)
AXBlade 采用 EIP-712 签名认证方式,配合 JWT Token 使用。这确保了所有交互都经过钱包私钥授权且无需用户在每次请求时进行复杂的链上交互。
认证流程
- 获取 Nonce - 根据钱包地址从服务器获取随机数(nonce)和签名数据结构。
- 签名消息 - 钱包使用私钥对包含 nonce 的结构化数据进行 EIP-712 签名。
- 提交登录 - 将签名提交给服务器,验证通过后获取 JWT Token。
- 携带 Token - 在所有私有接口(Private API)的请求头中携带该 Token。
EIP-712 Domain 配置
const domain = {
name: "AXBlade",
version: "1",
chainId: 421614, // Arbitrum Sepolia
verifyingContract: "0xFDe43f8e6e082975d246844DEF4fE8E704403d43"
};
1. 获取 Nonce
获取 用于签名的随机数和完整的 EIP-712 类型数据,以防止重放攻击。
端点: GET /api/v1/auth/nonce/:address
路径参数:
| 参数 | 类型 | 说明 |
|---|---|---|
address | string | 用户的钱包地址 (0x...) |
返回示例:
{
"nonce": 1,
"typed_data": {
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"Login": [
{ "name": "wallet", "type": "address" },
{ "name": "nonce", "type": "uint256" },
{ "name": "timestamp", "type": "uint256" }
]
},
"primaryType": "Login",
"domain": {
"name": "AXBlade",
"version": "1",
"chainId": 421614,
"verifyingContract": "0xFDe43f8e6e082975d246844DEF4fE8E704403d43"
},
"message": {
"wallet": "0x1234567890abcdef...",
"nonce": "1",
"timestamp": "1703577600"
}
}
}
2. 登录 (Login)
提交 EIP-712 签名以获取 JWT Token。
端点: POST /api/v1/auth/login
请求体:
| 字段 | 类型 | 说明 |
|---|---|---|
address | string | 钱包地址 |
signature | string | EIP-712 签名结果 |
timestamp | number | 签名时使用的 Unix 时间戳(秒) |
请求示例:
{
"address": "0x1234567890abcdef...",
"signature": "0x...",
"timestamp": 1703577600
}
返回示例:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": 1703664000
}
3. 使用 Token
获取 Token 后,需将其存储在客户端(如 LocalStorage)。在后续请求私有接口时,请在 HTTP Header 中包含:
Authorization: Bearer <your_jwt_token>
完整签名示例 (TypeScript)
import { ethers } from 'ethers';
const API_BASE = 'https://api.axblade.io/api/v1';
async function login(signer: ethers.Signer): Promise<string> {
const address = await signer.getAddress();
// 1. 获取 nonce 和类型数据
const nonceRes = await fetch(`${API_BASE}/auth/nonce/${address}`);
const { typed_data } = await nonceRes.json();
// 2. 使用钱包签名
const signature = await signer.signTypedData(
typed_data.domain,
{ Login: typed_data.types.Login },
typed_data.message
);
// 3. 登录获取 JWT
const loginRes = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address,
signature,
timestamp: parseInt(typed_data.message.timestamp)
})
});
const { token, expires_at } = await loginRes.json();
// 存储 token
localStorage.setItem('axblade_token', token);
localStorage.setItem('axblade_token_expires', expires_at.toString());
return token;
}
React Hook 示例
import { useState, useCallback } from 'react';
import { useAccount, useSignTypedData } from 'wagmi';
export function useAXBladeAuth() {
const { address } = useAccount();
const { signTypedDataAsync } = useSignTypedData();
const [token, setToken] = useState<string | null>(null);
const login = useCallback(async () => {
if (!address) throw new Error('Wallet not connected');
// 获取 nonce
const res = await fetch(`/api/v1/auth/nonce/${address}`);
const { typed_data } = await res.json();
// 签名
const signature = await signTypedDataAsync({
domain: typed_data.domain,
types: typed_data.types,
primaryType: 'Login',
message: typed_data.message,
});
// 登录
const loginRes = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address,
signature,
timestamp: parseInt(typed_data.message.timestamp)
})
});
const { token } = await loginRes.json();
setToken(token);
return token;
}, [address, signTypedDataAsync]);
return { token, login };
}
注意事项
- 签名有效期: 签名时间戳通常有 5 分钟的有效期限制,请确保系统时间准确。
- Token 有效期: JWT Token 默认 24 小时有效,过期后需重新登录。
- 安全存储: 请妥善存储 Token,避免 XSS 攻击导致 Token 泄露。