如何实现 DApp 的用户身份认证?有哪些常见方式?
DApp 用户身份认证有哪些方式?
DApp 的身份认证与传统 Web 应用完全不同——没有用户名密码,没有 Cookie Session,取而代之的是钱包签名、链上验证和去中心化标识。面试中常从"钱包连接"切入,逐步追问 SIWE、DID、ZKP 等进阶方案。
钱包连接:最基础的认证方式
钱包连接是 DApp 认证的起点。用户通过 MetaMask 等钱包授权 DApp 读取其以太坊地址,地址即为身份标识。
核心流程:调用 eth_requestAccounts 获取地址 → 验证地址格式 → 以地址作为用户唯一标识。
javascriptasync function connectWallet() { if (!window.ethereum) { throw new Error("请安装 MetaMask"); } const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); const address = accounts[0].toLowerCase(); if (!/^0x[a-f0-9]{40}$/i.test(address)) { throw new Error("地址格式无效"); } return address; }
局限:仅能证明用户拥有该地址,无法证明"是谁在操作"——同一地址可能被多人控制,也无法区分不同会话。这正是 SIWE 要解决的问题。
SIWE(Sign-In with Ethereum):当前主流认证标准
SIWE 是 ERC-4361 定义的标准协议,通过钱包签名一条结构化消息来证明身份,相当于 Web3 的"登录"。
认证流程:
- 后端生成随机 nonce,返回给前端
- 前端构造 EIP-4361 格式消息,请求钱包签名
- 后端通过
ecrecover从签名恢复出签名者地址 - 验证 nonce、过期时间、域名等字段,通过后签发 JWT Session
javascript// 前端:构造 SIWE 消息并签名 import { SiweMessage } from "siwe"; async function signInWithEthereum() { // 1. 从后端获取 nonce const nonce = await fetch("/api/nonce").then(r => r.text()); // 2. 构造 EIP-4361 标准消息 const message = new SiweMessage({ domain: window.location.host, address: await getAddress(), statement: "Sign in to DApp", uri: window.location.origin, version: "1", chainId: 1, nonce, issuedAt: new Date().toISOString(), expirationTime: new Date(Date.now() + 600000).toISOString() }); // 3. 请求钱包签名 const signature = await window.ethereum.request({ method: "personal_sign", params: [message.prepareMessage(), await getAddress()] }); // 4. 发送到后端验证 const res = await fetch("/api/verify", { method: "POST", body: JSON.stringify({ message, signature }) }); return res.ok; }
javascript// 后端:验证签名 const { SiweMessage } = require("siwe"); async function verifySiwe(message, signature) { const siweMessage = new SiweMessage(message); const result = await siweMessage.verify({ signature }); if (!result.success) throw new Error("签名验证失败"); // 检查 nonce 防重放、检查域名防钓鱼 if (result.data.nonce !== storedNonce) throw new Error("Nonce 不匹配"); return result.data.address; // 返回已验证的地址 }
为什么 SIWE 比单纯钱包连接更安全:nonce 防重放攻击,域名绑定防钓鱼,过期时间限制会话有效期,签名操作零 Gas 费。
去中心化身份(DID)与可验证凭证(VC)
DID 是 W3C 标准化的去中心化标识符,格式为 did:method:identifier(如 did:ethr:0x1234...)。与传统地址标识不同,DID 将公钥、服务端点等元数据记录在链上 DID 文档中,支持密钥轮换和多设备管理。
DID 与 VC 的协作模式:
- DID:用户的去中心化标识,链上存储 DID 文档
- VC(Verifiable Credential):由可信机构签发的凭证(如 KYC 认证、学历证明),以 DID 为主体
- 验证流程:持有者出示 VC → 验证者解析颁发者 DID → 链上验证签名 → 确认凭证有效性
javascript// 使用 did-jwt 库创建和验证 DID 相关凭证 import { createVerifiableCredentialJwt, verifyCredential } from "did-jwt-vc"; import { Resolver } from "did-resolver"; import { getResolver } from "ethr-did-resolver"; const resolver = new Resolver(getResolver({ rpcUrl: "https://mainnet.infura.io/v3/YOUR_KEY" })); // 验证者:验证 VC 的签名和有效期 async function verifyVC(jwt) { const verified = await verifyCredential(jwt, resolver); if (!verified.verified) throw new Error("VC 验证失败"); return verified.payload; // 返回凭证内容 }
DID 的优势:用户自主控制身份数据,可跨 DApp 复用,无需重复注册。劣势:生态碎片化(多种 DID 方法并存),链上解析延迟较高,密钥管理对普通用户门槛大。
零知识证明在身份认证中的应用
零知识证明允许用户证明某个声明(如"我已满 18 岁")而不暴露具体数据(如出生日期),适用于高隐私场景。
典型场景:KYC 合规验证——用户向 DApp 证明自己通过了 KYC,但不暴露姓名、身份证号等敏感信息。
实现路径(以 zk-SNARK 为例):
- 可信机构对用户身份数据生成承诺(commitment),签发 VC
- 用户在本地生成 ZK 证明:证明"持有某 VC 且满足条件(如 age ≥ 18)"
- DApp 验证链上证明,确认声明有效,不接触原始数据
solidity// 简化的链上 ZK 验证器(使用 Groth16) contract IdentityVerifier { function verifyProof( uint[2] memory a, uint[2][2] memory b, uint[2] memory c, uint[1] memory input // public input: 如 age_threshold 的 hash ) public returns (bool) { return IVerifier(verifier).verifyProof(a, b, c, input); } }
当前局限:证明生成耗时较长(数秒),Gas 费用高,开发门槛大。适合对隐私要求极高的金融和医疗场景,不建议在普通 DApp 中滥用。
方案对比与选型建议
| 方案 | 去中心化程度 | 实现难度 | 隐私保护 | 适用场景 |
|---|---|---|---|---|
| 钱包连接 | 高 | 低 | 低 | 基础 DApp 入口 |
| SIWE | 高 | 中 | 中 | 主流 DApp 登录 |
| DID + VC | 高 | 高 | 高 | 跨应用身份复用、合规 |
| ZKP 证明 | 高 | 很高 | 极高 | 隐私敏感型 DeFi、KYC |
选型原则:从钱包连接起步,引入 SIWE 做会话管理,需要跨应用身份互通时接入 DID,仅在强隐私需求时引入 ZKP。不要一开始就追求最去中心化的方案——用户体验和开发成本同样重要。
面试追问与要点
Q: SIWE 和单纯钱包签名有什么区别? 单纯钱包签名没有标准格式,消息内容、域名、过期时间全靠自定义,容易遭受重放和钓鱼攻击。SIWE 定义了 EIP-4361 标准消息格式,包含 nonce、domain、expiration-time 等字段,后端可系统性校验,安全性远高于自定义签名。
Q: DID 如何解决"跨 DApp 身份复用"问题? DID 文档存储在链上,任何 DApp 都可通过解析 DID 获取用户的公钥和服务端点。用户在一个 DApp 中通过 DID 注册后,其他 DApp 只需解析同一 DID 即可识别用户,无需重复提交信息。配合 VC,用户还可选择性披露凭证属性,实现最小化信息披露。
Q: ZKP 身份认证的性能瓶颈在哪? 主要瓶颈在证明生成阶段:Groth16 证明生成需要数秒到数十秒,且依赖可信设置(trusted setup)。验证阶段 Gas 费较高,一笔 Groth16 验证约 20-30 万 Gas。解决方案包括使用递归证明压缩、链下聚合验证,以及等待 ZK 硬件加速方案成熟。