Skip to main content

身份验证 (Authentication)

ZTDX API 使用 EIP-712 结构化签名进行身份验证。所有需要认证的接口都需要使用用户的以太坊私钥对 EIP-712 类型化数据进行签名。

EIP-712 签名标准

ZTDX 使用 EIP-712 (Ethereum Typed Structured Data Hashing and Signing) 标准进行签名验证。这是一种更加安全和用户友好的签名方式,钱包会清晰地显示签名的内容。

Domain Configuration

所有 EIP-712 签名使用以下 Domain 配置:

{
"name": "ZTDX",
"version": "1",
"chainId": <network_chain_id>,
"verifyingContract": "<vault_contract_address>"
}

参数说明:

  • chainId: 网络链 ID(如 Arbitrum Sepolia: 421614)
  • verifyingContract: ZTDX Vault 合约地址
重要

不同的网络环境(测试网/主网)使用不同的 chainIdverifyingContract。请从服务器配置或前端环境变量中获取正确的值。

登录流程 (Login Flow)

登录流程分为两步:1) 获取 Nonce,2) 签名登录

1. 获取 Nonce

在登录前,必须先获取用户的当前 nonce 值和用于签名的 EIP-712 typed data。

请求

  • Method: GET
  • Path: /api/v1/auth/nonce/:address
  • Authentication: 不需要

路径参数

参数类型必填描述
addressstring以太坊地址(0x开头,42字符)

响应示例

{
"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": "ZTDX",
"version": "1",
"chainId": 421614,
"verifyingContract": "0xYourVaultContractAddress"
},
"message": {
"wallet": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"nonce": "1",
"timestamp": "1704067200"
}
}
}

响应字段

字段类型描述
noncenumber当前用户的 nonce 值(每次成功登录后自增)
typed_dataobjectEIP-712 类型化数据,可直接用于钱包签名

Nonce 机制说明

  • Nonce 在用户首次访问时创建,初始值为 1
  • 每次成功登录后,nonce 会自动加 1
  • 多次调用 nonce 接口,在未登录前返回值保持不变
  • Nonce 用于防止重放攻击

2. 签名并登录

使用获取到的 typed_data 进行 EIP-712 签名,然后发送登录请求。

请求

  • Method: POST
  • Path: /api/v1/auth/login
  • Content-Type: application/json
  • Authentication: 不需要

请求参数

参数类型必填描述
addressstring以太坊地址(小写)
signaturestringEIP-712 签名(0x开头)
timestampnumberUnix 时间戳(),需在 5 分钟内有效
时间戳格式

timestamp 使用秒级时间戳,而不是毫秒。请确保与 typed_data 中的 timestamp 保持一致。

请求示例

{
"address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"signature": "0xabcdef123456...",
"timestamp": 1704067200
}

前端签名代码示例 (ethers.js v6)

import { BrowserProvider } from 'ethers';

async function loginToZTDX(address) {
// 1. 获取 nonce 和 typed data
const response = await fetch(`/api/v1/auth/nonce/${address}`);
const { nonce, typed_data } = await response.json();

// 2. 使用钱包进行 EIP-712 签名
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

// ethers v6 使用 signTypedData
const signature = await signer.signTypedData(
typed_data.domain,
{ Login: typed_data.types.Login },
typed_data.message
);

// 3. 发送登录请求
const loginResponse = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: address.toLowerCase(),
signature,
timestamp: parseInt(typed_data.message.timestamp)
})
});

const { token, expires_at } = await loginResponse.json();

// 保存 token
localStorage.setItem('jwt_token', token);

return { token, expires_at };
}

Python 签名代码示例

from eth_account import Account
from eth_account.messages import encode_typed_data
import requests
import time

def login_to_ztdx(privat_key: str, address: str) -> dict:
# 1. 获取 nonce 和 typed data
response = requests.get(f"/api/v1/auth/nonce/{address}")
data = response.json()
typed_data = data["typed_data"]

# 2. 使用 EIP-712 签名
signable_message = encode_typed_data(full_message=typed_data)
signed = Account.sign_message(signable_message, private_key=private_key)
signature = signed.signature.hex()

# 3. 发送登录请求
login_response = requests.post(
"/api/v1/auth/login",
json={
"address": address.lower(),
"signature": signature,
"timestamp": int(typed_data["message"]["timestamp"])
}
)

return login_response.json()

响应示例

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": 1704153600
}

响应字段

字段类型描述
tokenstringJWT 认证令牌
expires_atnumber令牌过期时间(Unix 时间戳,秒)

错误响应

HTTP 状态码错误码描述
400TIMESTAMP_EXPIRED时间戳已过期(超过5分钟)
400INVALID_SIGNATURE_FORMAT签名格式无效
401SIGNATURE_INVALIDEIP-712 签名验证失败
404USER_NOT_FOUND用户不存在,请先调用 nonce 接口
500DATABASE_ERROR数据库错误
500JWT_GENERATION_FAILEDJWT 生成失败

使用 JWT Token 进行认证

登录成功后,所有需要认证的 API 请求都需要在 HTTP Header 中携带 JWT Token:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

示例代码

// JavaScript/TypeScript
fetch('/api/v1/account/balances', {
headers: {
'Authorization': `Bearer ${token}`
}
});
# Python
import requests

headers = {
'Authorization': f'Bearer {token}'
}
response = requests.get('/api/v1/account/balances', headers=headers)

其他API的EIP-712签名

除了登录外,以下操作也需要 EIP-712 签名:

创建订单签名 (CreateOrder)

{
"types": {
"CreateOrder": [
{ "name": "wallet", "type": "address" },
{ "name": "symbol", "type": "string" },
{ "name": "side", "type": "string" },
{ "name": "orderType", "type": "string" },
{ "name": "price", "type": "string" },
{ "name": "amount", "type": "string" },
{ "name": "leverage", "type": "uint32" },
{ "name": "timestamp", "type": "uint256" }
]
},
"message": {
"wallet": "0x...",
"symbol": "BTCUSDT",
"side": "buy",
"orderType": "limit",
"price": "65000",
"amount": "0.1",
"leverage": 10,
"timestamp": "1704067200"
}
}

详见 订单管理 文档。

取消订单签名 (CancelOrder)

{
"types": {
"CancelOrder": [
{ "name": "wallet", "type": "address" },
{ "name": "orderId", "type": "string" },
{ "name": "timestamp", "type": "uint256" }
]
}
}

批量取消订单签名 (BatchCancelOrders)

{
"types": {
"BatchCancelOrders": [
{ "name": "wallet", "type": "address" },
{ "name": "orderIds", "type": "string" },
{ "name": "timestamp", "type": "uint256" }
]
},
"message": {
"wallet": "0x...",
"orderIds": "uuid1,uuid2,uuid3", // 逗号分隔
"timestamp": "1704067200"
}
}

创建推荐码签名 (CreateReferralCode)

{
"types": {
"CreateReferralCode": [
{ "name": "wallet", "type": "address" },
{ "name": "timestamp", "type": "uint256" }
]
}
}

绑定推荐码签名 (BindReferralCode)

{
"types": {
"BindReferralCode": [
{ "name": "wallet", "type": "address" },
{ "name": "code", "type": "string" },
{ "name": "timestamp", "type": "uint256" }
]
}
}

WebSocket 认证签名 (WebSocketAuth)

{
"types": {
"WebSocketAuth": [
{ "name": "wallet", "type": "address" },
{ "name": "timestamp", "type": "uint256" }
]
}
}

详见 WebSocket 文档

安全最佳实践

  1. 时间戳验证: 所有签名请求都必须包含时间戳,且必须在 5 分钟内有效
  2. Nonce 保护: Login nonce 在每次成功登录后自增,防止重放攻击
  3. 地址格式: 所有地址必须小写
  4. HTTPS: 生产环境必须使用 HTTPS
  5. Token 存储: 不要在 URL 或日志中暴露 JWT token
  6. Token 刷新: Token过期后需要重新登录

EIP-712 TypeHash 定义

系统使用的所有 EIP-712 TypeHash 定义:

// Login
Login(address wallet,uint256 nonce,uint256 timestamp)

// CreateOrder
CreateOrder(address wallet,string symbol,string side,string orderType,string price,string amount,uint32 leverage,uint256 timestamp)

// CancelOrder
CancelOrder(address wallet,string orderId,uint256 timestamp)

// BatchCancelOrders
BatchCancelOrders(address wallet,string orderIds,uint256 timestamp)

// CreateReferralCode
CreateReferralCode(address wallet,uint256 timestamp)

// BindReferralCode
BindReferralCode(address wallet,string code,uint256 timestamp)

// WebSocketAuth
WebSocketAuth(address wallet,uint256 timestamp)

常见问题

Q: 为什么使用 EIP-712 而不是 Personal Sign?

A: EIP-712 提供了更好的用户体验和安全性:

  • 钱包会清晰展示签名的结构化数据
  • 用户可以看到他们正在签名的确切内容
  • 更难被钓鱼攻击欺骗

Q: timestamp 过期时间是多少?

A: 5分钟。如果请求时间超过时间戳5分钟,会返回 TIMESTAMP_EXPIRED 错误。

Q: nonce 什么时候会增加?

A: 只在成功登录(/api/v1/auth/login 返回 200)后才会增加。登录失败或获取 nonce 接口都不会改变 nonce 值。

Q: 如何在 MetaMask 中测试 EIP-712 签名?

A: 使用 eth_signTypedData_v4 方法:

const signature = await ethereum.request({
method: 'eth_signTypedData_v4',
params: [address, JSON.stringify(typed_data)]
});

相关文档