jsx
"use client";
import type { Account } from "@ant-design/web3";
import { ConnectButton, Connector } from "@ant-design/web3";
import { Flex, Space } from "antd";
import React from "react";
import { JwtProvider } from "./JwtProvider";
export default function App() {
const jwt = React.useContext(JwtProvider);
SIWE:以太坊登录新标准实践指南
SIWE:以太坊登录的强大工具
SIWE (Sign-In with Ethereum) 是一种在以太坊上验证用户身份的方法。类似于发起交易,它通过签名来证明用户对钱包的控制权。目前,大多数主流钱包插件都已支持这种简单的身份验证方式。
本文主要讨论以太坊上的签名场景,不涉及其他公链如 Solana、SUI 等。
什么时候需要 SIWE?
如果你的 Dapp 具有以下需求,可以考虑使用 SIWE:
对于主要提供查询功能的 Dapp(如区块浏览器),可能并不需要 SIWE。
虽然通过钱包连接可以在前端证明身份,但对于需要后端支持的 API 调用,仅传递地址是不够的,因为地址是公开信息,任何人都可以使用。
SIWE 的原理和流程
SIWE 的流程可以概括为三个步骤:连接钱包 - 签名 - 获取身份标识。
连接钱包
这是 Web3 应用中常见的操作,通过钱包插件连接用户的钱包。
签名
签名步骤包括获取 Nonce 值、钱包签名和后端签名校验。
首先从后端获取随机生成的 Nonce 值,后端会将其与当前地址关联。
前端获取 Nonce 值后,构建签名内容,包括 Nonce、域名、链 ID 等信息,然后使用钱包的签名方法进行签名。
最后将签名发送给后端进行验证。
获取身份标识
后端验证签名通过后,会返回用户身份标识(如 JWT)。前端后续请求时带上地址和身份标识,即可证明对钱包的所有权。
实践 SIWE
下面我们通过一个简单的 Demo 来实践 SIWE 的基本流程,目标是让 Dapp 能返回用于身份校验的 JWT。
注意:此 Demo 仅用于展示基本流程,生产环境使用可能存在安全隐患。
准备工作
我们使用 Next.js 开发全栈应用,需要准备 Node.js 环境。
安装依赖
首先安装 Next.js:
npx create-next-app@14
按提示完成安装。
进入项目目录并启动:
npm run dev
访问 localhost:3000 可以看到基本的 Next.js 项目。
安装 SIWE 相关依赖
我们使用 Ant Design Web3 来连接钱包并实现 SIWE 功能:
npm install antd @ant-design/web3 @ant-design/web3-wagmi wagmi viem @tanstack/react-query --save
引入 Wagmi
在 layout.tsx 中引入 WagmiProvider:
jsx "use client"; import { getNonce, verifyMessage } from "@/app/api"; import { Mainnet, MetaMask, OkxWallet, TokenPocket, WagmiWeb3ConfigProvider, WalletConnect, } from "@ant-design/web3-wagmi"; import { QueryClient } from "@tanstack/react-query"; import React from "react"; import { createSiweMessage } from "viem/siwe"; import { http } from "wagmi"; import { JwtProvider } from "./JwtProvider";
const YOUR_WALLET_CONNECT_PROJECT_ID = "c07c0051c2055890eade3556618e38a6"; const queryClient = new QueryClient();
const WagmiProvider: React.FC = ({ children }) => { const [jwt, setJwt] = React.useState(null);
return ( <wagmiweb3configprovider siwe="{{" getnonce:="" async="" (address)=""> (await getNonce(address)).data, createMessage: (props) => { return createSiweMessage({ ...props, statement: "Ant Design Web3" }); }, verifyMessage: async (message, signature) => { const jwt = (await verifyMessage(message, signature)).data; setJwt(jwt); return !!jwt; }, }} chains={[Mainnet]} transports={{ [Mainnet.id]: http(), }} walletConnect={{ projectId: YOUR_WALLET_CONNECT_PROJECT_ID, }} wallets={[ MetaMask(), WalletConnect(), TokenPocket({ group: "Popular", }), OkxWallet(), ]} queryClient={queryClient} > {children} ); };
export default WagmiProvider;
然后添加连接钱包的按钮:
jsx "use client"; import type { Account } from "@ant-design/web3"; import { ConnectButton, Connector } from "@ant-design/web3"; import { Flex, Space } from "antd"; import React from "react"; import { JwtProvider } from "./JwtProvider";
export default function App() { const jwt = React.useContext(JwtProvider);
const renderSignBtnText = ( defaultDom: React.ReactNode, account?: Account ) => { const { address } = account ?? {}; const ellipsisAddress = address ? ${address.slice(0, 6)}...${address.slice(-6)} : ""; return Sign in as ${ellipsisAddress}; };
return ( <>
这样就实现了一个基本的 SIWE 登录框架。
接口实现
现在来实现后端需要的一些接口。
Nonce
生成随机 Nonce 并与地址关联:
javascript import { randomBytes } from "crypto"; import { addressMap } from "../cache";
export async function GET(request: Request) { const { searchParams } = new URL(request.url); const address = searchParams.get("address");
if (!address) { throw new Error("Invalid address"); } const nonce = randomBytes(16).toString("hex"); addressMap.set(address, nonce); return Response.json({ data: nonce, }); }
verifyMessage
验证签名并返回 JWT:
javascript import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; import jwt from "jsonwebtoken"; import { parseSiweMessage } from "viem/siwe"; import { addressMap } from "../cache";
const JWT_SECRET = "your-secret-key"; // 请使用更安全的密钥,并添加对应的过期校验等
const publicClient = createPublicClient({ chain: mainnet, transport: http(), });
export async function POST(request: Request) { const { signature, message } = await request.json();
const { nonce, address = "0x" } = parseSiweMessage(message);
if (!nonce || nonce !== addressMap.get(address)) { throw new Error("Invalid nonce"); }
const valid = await publicClient.verifySiweMessage({ message, address, signature, });
if (!valid) { throw new Error("Invalid signature"); }
const token = jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }); return Response.json({ data: token, }); }
至此,一个基本的 SIWE 登录 Dapp 就开发完成了。
性能优化
为提高验证速度,建议使用专门的节点服务。这里我们使用 ZAN 的节点服务,替换 publicClient 的默认 RPC:
javascript const publicClient = createPublicClient({ chain: mainnet, transport: http('), //替换为获取到的 ZAN 节点服务 RPC });
这样可以显著减少验证时间,提高接口速度。