SIWE使用手册:如何让你的Dapp更强大? ZAN Team |2024-12-31 16:00 SIWE(Sign-In with Ethereum),是一种在Ethereum 上对用户身份的一种验证方式,和钱包发起一笔交易类似,表明用户对该钱包有控制权。
本文的介绍遵循EIP-4361 Sign-In with Ethereum 规则
SIWE(Sign-In with Ethereum),是一种在Ethereum 上对用户身份的一种验证方式,和钱包发起一笔交易类似,表明用户对该钱包有控制权。
目前的身份验证方式已经非常简单,只需要在钱包插件中对资讯进行签名即可,常见的钱包插件都已经支援。
本文考虑的签名场景是在Ethereum 上,其他的像Solana、SUI 等不在本文的讨论范围内。
你的专案需要SIWE 吗
SIWE 是为了解决钱包位址的身份验证问题,所以如果你有需求,可以考虑使用SWIE:
但如果你的Dapp 是查询为主的功能,例如像etherscan 这类应用,没有SIWE 也是可以的。
可能你会有一个疑问,在Dapp 上我透过钱包进行连线之後,不就代表了我有钱包的所有权了吗。
对,又不完全对。对於前端来说,确实你通过钱包连接的操作之後,你表明了你的身份,但是对於一些需要後端支持的接口调用,你是没有办法表明自己的身份的,如果只是在接口中传你的地址的话,那麽谁都可以「借用」你的身分了,毕竟地址是公开的资讯。
SIWE 的原理和流程
SIWE 的流程总结起来就是三个步骤:连结钱包– 签名– 取得身分识别识别。我们对这三个步骤展开详细介绍。
连接钱包
连接钱包是一个常见的WEB3 操作,透过钱包插件的方式可以在Dapp 中连接你的钱包。
签名
在SIWE 中,签署的步骤包括了取得Nonce 值,钱包签章以及後端签章校验。
取得Nonce 值应该是参考了ETH 交易中的Nonce 值的设计,也是需要呼叫後端的介面来取得。後端在接受到请求之後,回生成随机的Nonce 值,并和当前的地址进行关联,为後面的签名做准备。
前端取得到Nonce 值之後,就需要建构签章内容,SIWE 可以设计的签章内容包含取得到的Nonce 值、网域名称、链ID、签章的内容等,我们一般会使用钱包提供的签章方法来对内容进行签名。
在建置完签名之後,最後将签名传送给後端。
取得取得身分标识
後端在校验完签名并且通过之後,会返回对应的用户身份标识,可以是JWT,前端後续在发送後端请求时带上对应的地址和身份标识,就可以表明自己对钱包的所有权了。
实践一下
目前已经有很多的元件、库支援开发者快速的存取钱包连接和SIWE 了,我们可以实际操作一下,实践的目标,是能够让你的Dapp 能够返回JWT 用於用户身份校验。
注意,这个DEMO 只是用来介绍SIWE 的基本流程,使用在生产环境可能会有安全问题。
事先准备
本文采用nextjs 的方式开发应用,因此需要开发者准备好nodejs 的环境。采用nextjs 的一个好处在於,我们可以直接开发全端的项目,不需要分割成前後端两个项目。
安装依赖
首先我们安装nextjs,在你的专案目录里,用命令列输入:
npx create-next-app@14
依照指示安装好nextjs,可以看到下面的内容:
进入到专案目录之後,可以看到nextjs 鹰架已经帮我们做了很多的工作了。我们可以在专案目录里面将项目跑起来:
npm run dev
之後根据终端机的提示,进入到localhost: 3000
就可以看到一个基本的nextjs 专案已经跑起来了。
安装SIWE 相关依赖
根据先前的介绍,SIWE 需要依赖登入体系,因此需要将我们的专案连接上钱包,这里我们使用Ant Design Web3( https://web3.ant.design/ ),因为:
- 它完全免费,目前仍在积极维护中
- 作为WEB3 元件库,它的使用体验和普通元件库类似,没有额外的心智负担
- 并且支援SIWE。
我们需要在终端输入:
npm install antd @ant-design/web3 @ant-design/web3-wagmi wagmi viem @tanstack/react-query --save
引入Wagmi
Ant Design Web3 的SIWE 是依赖Wagmi 函式库来实现的,所以在专案中需要引入相关的元件。我们在layout.tsx
中引入对应的Provider,这样整个专案都可以使用Wagmi 提供的Hooks。
我们先定义WagmiProvider 的配置,程式码如下:
"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 ( (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;
我们使用了Ant Design Web3 提供的Provider,并对SIWE 的一些介面做了定义,具体介面的实作我们在後续会介绍。
之後我们再引入连接钱包的按钮,这样就可以在前端中加入了一个连接的入口。
至此位置就算已经接上了SIWE,步骤非常简单。
之後我们需要定义一个连接的按钮,来实现连接钱包和签名,程式码如下:
"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 ( <>
{jwt}
); }
这样子我们就实作了一个最简单的SIWE 登入框架。
介面实现
根据上文的介绍,SIWE 需要一些的介面来帮助後端校验使用者的身分。现在我们来简单实作一下。
Nonce
Nonce 的是为了让钱包在签名时每次产生的签名内容变化,提高签名的可靠性。这个Nonce 的产生需要和用户传入的address 产生关联,提高验证的准确性。
Nonce 的实作非常直接,首先我们产生一个随机的字串(由字母和数字产生),之後再将这个nonce 和address 建立联系即可,程式码如下:
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, }); }
signMessage
signMessage 的功能是签名内容,这部分功能一般是透过钱包插件完成,我们一般不需要做配置,只需要指定方法即可,在本Demo 中使用的是Wagmi 的签名方法。
verifyMessage
在使用者对内容进行签署之後,需要将签名前的内容和签名一同发给後端进行校验,後端从签名中解析出对应的内容进行比较,一致则表示验证通过。
此外,对於签署的内容还需要再做一些安全性的校验,例如签名内容中的Nonce 值是否和我们派发给使用者的一致等。在验证通过之後,需要传回对应的使用者JWT 用於後续的权限校验,范例程式码如下:
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); console.log("nonce", nonce, address, addressMap); // 校验nonce 值是否一致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"); } // 生成jwt 并返回const token = jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }); return Response.json({ data: token, }); }
至此,一个基本实作SIWE 登入的Dapp 就开发完成了。
一些优化项
现在我们在进行SIWE 登入时,如果我们使用预设的RPC 节点的话,验证的过程将会花费近30s 的时间,所以这里强烈建议使用专门的节点服务来提升介面的回应时间。本文所使用的是ZAN 的节点服务( https://zan.top/home/node-service?chInfo=ch_WZ ),可以前往ZAN 节点服务控制台取得对应的RPC 连线。
我们在取得到到以太坊主网的HTTPS RPC 连线之後,在程式码中替换掉publicClient
的预设RPC:
const publicClient = createPublicClient({ chain: mainnet, transport: http('https://api.zan.top/node/v1/eth/mainnet/xxxx'), //获取到的ZAN 节点服务RPC });
替换之後,验证的时间可以显着减少,介面的速度显着加快。
深度ETH用户POS以太坊