本文的介绍遵循 EIP-4361 Sign-In with Ethereum 规则

SIWE(Sign-In with Ethereum),是一种在 Ethereum 上对用户身份的一种验证方式,和钱包发起一笔交易类似,表明用户对该钱包有控制权。

目前的身份验证方式已经非常简单,只需要在钱包插件中对信息进行签名即可,常见的钱包插件都已经支持。

本文考虑的签名场景是在 Ethereum 上,其他的像 Solana、SUI 等不在本文的讨论范围内。

SIWE使用手册:如何让你的Dapp更加强大?

你的项目需要 SIWE 吗

SIWE 是为了解决钱包地址的身份验证问题,所以如果你有一下需求,可以考虑使用 SWIE:

    • 你的 Dapp 有自己的用户体系;
    • 需要查询的信息和用户隐私相关;

但如果你的 Dapp 是一个查询为主的功能,比如像 etherscan 这类应用,没有 SIWE 也是可以的。

可能你会有一个疑问,在 Dapp 上我通过钱包进行连接之后,不就代表了我有钱包的所有权了吗。

对,又不完全对。对于前端来说,确实你通过钱包连接的操作之后,你表明了你的身份,但是对于一些需要后端支持的接口调用,你是没有办法表明自己的身份的,如果只是在接口中传你的地址的话,那么谁都可以「借用」你的身份了,毕竟地址是公开的信息。

SIWE 的原理和流程

SIWE 的流程总结起来就是三个步骤:连接钱包 -- 签名 -- 获取身份标识。我们对这三个步骤展开详细介绍。

SIWE使用手册:如何让你的Dapp更加强大?

SIWE使用手册:如何让你的Dapp更加强大?

SIWE使用手册:如何让你的Dapp更加强大?

连接钱包

连接钱包是一个常见的 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,可以看到下面的内容:

SIWE使用手册:如何让你的Dapp更加强大?

进入到项目目录之后,可以看到 nextjs 脚手架已经帮我们做了很多的工作了。我们可以在项目目录里面将项目跑起来:

npm run dev

之后根据终端的提示,进入到 localhost: 3000就可以看到一个基本的 nextjs 项目已经跑起来了。

SIWE使用手册:如何让你的Dapp更加强大?

安装 SIWE 相关依赖

根据之前的介绍,SIWE 需要依赖登录体系,因此需要将我们的项目连接上钱包,这里我们使用 Ant Design Web3(https://web3.ant.design/),因为:

  1. 它完全免费,并且目前还在积极维护中
  2. 作为 WEB3 组件库,它的使用体验和普通组件库类似,没有额外的心智负担
  3. 并且支持 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 连接。

SIWE使用手册:如何让你的Dapp更加强大?

我们在获取到到以太坊主网的 HTTPS RPC 连接之后,在代码中替换掉publicClient的默认 RPC:

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http('https://api.zan.top/node/v1/eth/mainnet/xxxx'), //获取到的 ZAN 节点服务 RPC
});

替换之后,验证的时间可以显著减少,接口的速度显著加快。