使用 SIWE 讓你的 Dapp 更加強大
本文的介紹遵循 EIP-4361 Sign-In with Ethereum 規則
SIWE(Sign-In with Ethereum),是一種在 Ethereum 上對用戶身份的一種驗證方式,和錢包發起一筆交易類似,表明用戶對該錢包有控製權。
目前的身份驗證方式已經非常簡單,只需要在錢包插件中對信息進行簽名即可,常見的錢包插件都已經支持。
本文考慮的簽名場景是在 Ethereum 上,其他的像 Solana、SUI 等不在本文的討論範圍內。
你的項目需要 SIWE 嗎
SIWE 是為了解決錢包地址的身份驗證問題,所以如果你有一下需求,可以考慮使用 SWIE:
○ 你的 Dapp 有自己的用戶體系;
○ 需要查詢的信息和用戶隱私相關;
但如果你的 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,因為:
- 它完全免費,並且目前還在積極維護中
- 作為 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<React.PropsWithChildren> = ({ children }) => {
const [jwt, setJwt] = React.useState<string | null>(null);
return (
<WagmiWeb3ConfigProvider
eip6963={{
autoAddInjectedWallets: true,
}}
ens
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}
>
<JwtProvider.Provider value={jwt}>{children}</JwtProvider.Provider>
</WagmiWeb3ConfigProvider>
);
};
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 (
<>
<Flex vertical>
<Connector
modalProps={{
mode: "simple",
}}
>
<ConnectButton signBtnTextRender={renderSignBtnText} />
</Connector>
<div className="text-center">{jwt}</div>
</Flex>
</>
);
}
這樣子我們就實現了一個最簡單的 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 的節點服務,可以前往 ZAN 節點服務控製臺獲取對應的 RPC 連接。
我們在獲取到到以太坊主網的 HTTPS RPC 連接之後,在代碼中替換掉publicClient的默認 RPC:
const publicClient = createPublicClient({
chain: mainnet,
transport: http('https://api.zan.top/node/v1/eth/mainnet/xxxx'), //获取到的 ZAN 节点服务 RPC
});
替換之後,驗證的時間可以顯著減少,接口的速度顯著加快。
关于 ZAN
ZAN 是蚂蚁数科旗下 Web3 科技品牌,致力于 Web3 应用优化--降低成本、增强安全和提升性能,围绕 Web3 应用全生命周期,提供可靠、稳定安全、定制化的产品和服务。依托 AntChain OpenLabs 的 TrustBase 开源开放技术体系,ZAN 拥有 Web3 领域独特的优势和创新能力,为 Web3 社区的区块链应用开发、企业和开发者的 Web3 应用提供了全面的技术产品和服务,其中包括节点服务(ZAN Node Service)、zk 加速(ZAN PowerZebra)、身份验证eKYC(ZAN Identity)以及智能合约审计(ZAN Smart Contract Review)等。
联系我们
Website | X | Discord | Telegram