导语:链上的"你是谁",始于一次签名
Web2 的身份是"账号 + 密码 + 手机验证码"。Web3 的身份回到原初:你拥有私钥,就能证明你是那个地址。但"证明"不是把私钥交出去,而是签名——用密码学零知识地证明"我掌握这把钥匙"。
SIWE(Sign-In With Ethereum)把签名登录标准化:一条规范的消息 + 一个签名 = 一次安全的登录凭证。它解决了"钱包登录 DApp"时最常见的三个坑:重放攻击、钓鱼签名、session 失效。
一句话总结:Web3 身份 = 私钥证明 + 标准化消息 + 会话管理;SIWE 是把"钱包连接"升级为"密码学验证的登录"的桥梁。
1. 从"连接钱包"到"签名登录"
1.1 连接钱包 ≠ 登录
旧做法:DApp 请求钱包连接(eth_requestAccounts)
→ 前端拿到地址 → 认为"用户已登录"
❌ 地址是公开的,任何人都能伪造"是这个地址"
❌ 没有密码学证据,服务端无法验证身份
❌ 无法跟踪 session、无法吊销
正确做法:签名登录
→ 服务端生成一条消息(含随机 nonce 与过期时间)
→ 用户用私钥签名
→ 服务端用地址公钥验签 → 确认"地址持有人"
→ 发放 session token
1.2 一次 SIWE 流程
1. DApp 请求 → 后端生成 SIWE 消息
(含 domain、address、uri、nonce、issued-at、expiration)
2. 前端把消息给钱包签名(personal_sign)
3. 后端校验:地址与签名匹配 + nonce 有效 + 未过期 + domain 匹配
4. 校验通过 → 建立 session(签发 JWT / 存会话)
5. 用户后续请求携带 session token
2. SIWE 消息规范
SIWE(EIP-4361)定义了消息的精确格式,防止跨站/跨应用重放:
<domain> wants you to sign in with your Ethereum account:
<address>
I accept the Terms of Use
URI: <uri>
Version: 1
Chain ID: <chain_id>
Nonce: <random_string>
Issued At: <ISO8601>
Expiration Time: <ISO8601> (可选)
Not Before: <ISO8601> (可选)
Request ID: <request_id> (可选)
Resources: (可选)
- <resource_url>
2.1 关键字段的安全作用
| 字段 | 防什么攻击 |
|---|---|
domain | 防跨站重放(Phishing)——签名只能用于该域名 |
address | 明确签署主体 |
nonce | 防重放(Replay)——一次性随机数 |
Issued At | 会话新鲜度 |
Expiration Time | 会话时效 |
URI | 绑定具体应用 |
2.2 签名与验签
// 前端:用钱包签名
import { SiweMessage } from "siwe";
const siwe = new SiweMessage({
domain: window.location.host,
address: wallet.address,
uri: window.location.origin,
version: "1",
chainId: 1,
nonce: await getNonce(), // 从后端获取
issuedAt: new Date().toISOString(),
});
const message = siwe.prepareMessage();
const signature = await wallet.signMessage(message);
// 后端:验证签名
import { SiweMessage } from "siwe";
const siwe = new SiweMessage(message);
const verified = await siwe.verify({ signature, nonce });
if (verified.success) {
// 确认真实地址,创建 session
const address = verified.data.address;
// 校验 domain 与当前请求 host 一致
}
一句话总结:SIWE 是把"签名登录"规范化的元协议——domain + nonce + 时间戳三件套,堵死了跨站重放与钓鱼签名两大漏洞。
3. Session 管理与安全细节
3.1 Session 生命周期
登录成功 → 签发短期 access token(如 JWT 15min)
→ 签发 refresh token(如 7 天,HttpOnly Cookie)
→ 过期 → 用 refresh token 续签(需重新验签?)
→ 退出 → 吊销 session
重要:签名只在登录/续签时发生,不每次请求都签
3.2 关键安全决策
| 决策 | 推荐 |
|---|---|
| token 存储 | 服务端 session 或 HttpOnly Cookie(防 XSS) |
| 签名消息 | 永远让用户看清内容(EIP-712 可读) |
| nonce 管理 | 一次性 + 短期有效(5 分钟) |
| domain 校验 | 服务端强制匹配请求 Host |
| 重放防护 | 服务端记录已用 nonce |
| 验签错误 | 统一返回,不泄露细节 |
3.3 服务端验签伪代码
def verify_login(message, signature, expected_domain):
# 1. 解析并校验格式
siwe = parse_siwe(message)
assert siwe.domain == expected_domain, "domain mismatch"
# 2. 校验 nonce 未使用且未过期
assert nonce_store.is_valid(siwe.nonce), "invalid nonce"
# 3. 校验时间窗
assert now_between(siwe.issued_at, siwe.expiration_time)
# 4. 密码学验签
addr = recover_address(message, signature)
assert addr == siwe.address, "address mismatch"
# 5. 消耗 nonce,防止重放
nonce_store.consume(siwe.nonce)
return create_session(addr)
一句话总结:签名是登录的"第一步",session 才是日常的"通行证"——把签名频率降到登录/续签,用 HttpOnly cookie + nonce 一次性把安全边界立住。
4. 身份层:DID 与可验证凭证
4.1 DID(去中心化标识符)
DID 结构:did:method:identifier
例:did:ethr:0x...(以太坊地址作为身份)
did:web:example.com(域名绑定身份)
did:key:z6Mk...(公钥直接作为身份)
DID 的特点:
- 自控(Self-sovereign):不由单一机构签发
- 可解析(Resolution):从 DID → 元数据(公钥、服务端点)
- 可移植:跨应用复用同一身份
4.2 Verifiable Credential(可验证凭证)
场景:学历、KYC、会员、票务
签发者(Issuer)→ 签发 VC(签名)
持有人(Holder)→ 存储并选择性披露
验证者(Verifier)→ 验签 + 校验凭证
选择性披露(Zero-Knowledge):
只证明"我年龄>18"而不泄露生日 → ZK 技术实现
4.3 身份系统分层
应用身份(SIWE session)→ 授权访问应用
身份标识(DID / ENS) → 统一的名字/身份
凭证(VC) → 可验证的属性证明
5. ENS:从地址到名字
ENS(Ethereum Name Service)把 0xAbc... 变成 alice.eth:
ENS 工作原理:
namehash:递归哈希解析域名树
resolver:将名字解析到地址/内容(合约)
注册:ENS 根合约管理子域名
价值:
- 可读地址:钱包收款、身份展示
- 子域名:组织身份(team.eth)
- 解析器:绑定邮箱、头像、社交资料(ENS Avatar)
5.1 ENS 与 SIWE 结合
// 用 ENS 名字作为用户界面,底层仍是地址
const name = await ens.lookupAddress(wallet.address);
// 前端显示 alice.eth,验签仍针对底层地址
6. 账户抽象:身份即合约
ERC-4337 把"身份"从 EOA(外部账户)升级为智能账户——身份规则可编程:
6.1 传统 vs 智能账户
| 维度 | EOA | ERC-4337 智能账户 |
|---|---|---|
| 恢复 | 私钥丢失=丢失 | 可社交恢复(多签名) |
| 权限 | 全有或全无 | 可设置限额/角色 |
| 批处理 | 逐个交易 | UserOperation 批量 |
| 付费 | 必须 ETH | 可代付(Gasless) |
| 登录 | 仅私钥签名 | 支持 WebAuthn、邮箱 |
6.2 一个智能账户的登录
用户通过任意方式(如 WebAuthn)发起登录意图
→ 构造 UserOperation(含验证 + 执行数据)
→ Bundler 打包 → EntryPoint 合约执行
→ 智能账户合约验证权限 → 执行操作
6.3 身份演进
EOA(一个私钥=一个身份)
↓ 账户抽象
智能账户(可恢复、可编程、可多因子)
↓ 未来
链上身份 + AI 代理(Agent 持有权限委托)
7. 常见攻击与防护
| 攻击 | 手法 | 防护 |
|---|---|---|
| 钓鱼签名 | 诱导签任意数据 | SIWE + EIP-712 可读 |
| 重放 | 复用旧签名 | nonce + 时间窗 |
| 中间人 | 篡改消息 | 验签校验全字段 |
| 恶意 DApp | 请求超额权限 | 最小权限、每请求授权 |
| 地址混淆 | 相似地址 | ENS + 显示警告 |
| 重入 | 交易内多次调用 | 状态机 + 权限校验 |
7.1 最小权限原则
签名策略:
交易签名:一次性、限金额、EIP-712
授权签名:限 spender、限额度、可撤销
登录签名:仅登录意图,不包含授权
8. 工程落地:一个完整的 SIWE 登录系统
8.1 目录结构
frontend/
├─ useSiwe.ts # 封装登录逻辑
└─ components/ # 连接钱包 + 签名按钮
backend/
├─ siwe/verify.ts # 验签路由
├─ siwe/nonce.ts # nonce 签发
└─ session.ts # session 管理
8.2 完整登录钩子
async function handleLogin(provider: Eip1193Provider, backend: Backend) {
// 1. 连接钱包
const wallet = new BrowserProvider(provider);
const signer = await wallet.getSigner();
const address = await signer.getAddress();
// 2. 获取 nonce
const nonce = await backend.getNonce();
// 3. 构造并签署 SIWE 消息
const siwe = new SiweMessage({
domain: window.location.host,
address,
uri: window.location.origin,
version: "1",
chainId: await wallet.getNetwork().then((n) => n.chainId),
nonce,
issuedAt: new Date().toISOString(),
});
const signature = await signer.signMessage(siwe.prepareMessage());
// 4. 提交后端验签
const session = await backend.verify({ message: siwe, signature });
return session; // 后续请求带 session token
}
8.3 服务端路由
// 后端:nonce 签发
app.get("/api/nonce", (req, res) => {
const nonce = generateRandomNonce();
nonceStore.set(nonce, { ttl: 5 * 60_000 });
res.json({ nonce });
});
// 后端:验签 + 建 session
app.post("/api/verify", async (req, res) => {
const { message, signature } = req.body;
const siwe = new SiweMessage(message);
const { data, success } = await siwe.verify({
signature,
domain: req.headers.host, // 强制域名校验
});
if (!success) return res.status(401).json({ error: "invalid" });
const session = await createSession(data.address);
res.cookie("session", session.token, { httpOnly: true, sameSite: "lax" });
res.json({ ok: true });
});
9. 身份的未来:跨链与自主权
- 跨链身份:一个 DID 映射多链地址(DID 解析器聚合)
- 自主权:用户掌握身份数据(而非平台)
- 身份聚合:社交 + 链上 + 凭证的合成身份
- AI 代理:Agent 用授权密钥代表用户操作(能力边界可编程)
9.1 能力边界示例
// 智能账户中的委托权限
mapping(address => Permission) public permissions;
struct Permission {
uint256 dailyLimit;
address[] allowedApps;
bool canTransfer;
}
10. 总结
- 身份本质:私钥签名证明"你是那个地址"
- SIWE:domain + nonce + 时间窗,标准化安全登录
- Session:签名仅一次,日常走 token(HttpOnly)
- DID / VC:从地址到可验证身份的扩展
- ENS:可读名字包装底层地址
- 账户抽象:身份从 EOA 演进为可编程合约
- 防护:最小权限 + 可读签名 + 全字段校验
延伸阅读:
- 区块链密码学基础 — 签名与哈希的数学底座
- 钱包与 HD 分层确定性钱包 — 身份背后的密钥管理
- 智能合约安全审计与常见漏洞 — 签名相关的攻击面
- 区块链-web3 专题 — DApp 集成钱包登录的实践
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。