使用以太坊登录 (SIWE)

通过 Sign in with Ethereum (SIWE) 插件,用户可以使用其以太坊钱包遵循 ERC-4361 标准 进行身份验证。该插件具有高度灵活性,允许您自行实现消息验证和 nonce 生成逻辑。

安装

添加服务器插件

将 SIWE 插件添加到您的认证配置中:

auth.ts
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [
        siwe({
            domain: "example.com",
            emailDomainName: "example.com", // 可选
            anonymous: false, // 可选,默认为 true
            getNonce: async () => {
                // 在此处实现您的 nonce 生成逻辑
                return "your-secure-random-nonce";
            },
            verifyMessage: async (args) => {
                // 在此处实现您的 SIWE 消息验证逻辑
                // 这应该根据消息验证签名
                return true; // 如果签名有效则返回 true
            },
            ensLookup: async (args) => {
                // 可选:实现 ENS 查找以获取用户名和头像
                return {
                    name: "user.eth",
                    avatar: "https://example.com/avatar.png"
                };
            },
        }),
    ],
});

迁移数据库

运行迁移或生成模式,以向数据库添加必要的字段和表。

npx @better-auth/cli migrate
npx @better-auth/cli generate

请参阅 Schema 部分以手动添加字段。

添加客户端插件

auth-client.ts
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
    plugins: [siweClient()],
});

使用方法

生成 Nonce

在签署 SIWE 消息之前,您需要为钱包地址生成一个 nonce:

generate-nonce.ts
const { data, error } = await authClient.siwe.nonce({
  walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
  chainId: 1, // 可选,默认为 1(以太坊主网)
});

if (data) {
  console.log("Nonce:", data.nonce);
}

使用以太坊登录

生成 nonce 并创建 SIWE 消息后,验证签名以进行身份验证:

sign-in-siwe.ts
const { data, error } = await authClient.siwe.verify({
  message: "您的 SIWE 消息字符串",
  signature: "0x...", // 来自用户钱包的签名
  walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
  chainId: 1, // 可选,默认为 1
  email: "user@example.com", // 可选,当 anonymous 为 false 时必需
});

if (data) {
  console.log("身份验证成功:", data.user);
}

配置选项

服务器选项

SIWE 插件接受以下配置选项:

  • domain: 您的应用程序的域名(SIWE 消息生成必需)
  • emailDomainName: 在不使用匿名模式时创建用户账户的电子邮件域名。默认为您基础 URL 中的域名
  • anonymous: 是否允许无需电子邮件的匿名登录。默认为 true
  • getNonce: 为每次登录尝试生成唯一 nonce 的函数。您必须实现此函数以返回加密安全的随机字符串。必须返回 Promise<string>
  • verifyMessage: 验证已签名的 SIWE 消息的函数。接收消息详细信息并应返回 Promise<boolean>
  • ensLookup: 可选的函数,用于查找以太坊地址的 ENS 名称和头像

客户端选项

SIWE 客户端插件不需要任何配置选项,但为了未来扩展性,您可以根据需要传递配置:

auth-client.ts
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    siweClient({
      // 可选的客户端配置可以放在这里
    }),
  ],
});

数据表结构

SIWE 插件添加了一个 walletAddress 表来存储用户钱包关联信息:

字段类型描述
idstring主键
userIdstring关联到 user.id
addressstring以太坊钱包地址
chainIdnumber链 ID(例如:1 表示以太坊主网)
isPrimaryboolean是否为用户的主钱包
createdAtdate创建时间戳

示例实现

以下是一个完整的示例,展示如何实现 SIWE(Sign-In with Ethereum)认证:

auth.ts
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
import { generateRandomString } from "better-auth/crypto";
import { verifyMessage, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

export const auth = betterAuth({
  database: {
    // 你的数据库配置
  },
  plugins: [
    siwe({
      domain: "myapp.com",
      emailDomainName: "myapp.com",
      anonymous: false,
      getNonce: async () => {
        // 生成加密安全的随机 nonce
        return generateRandomString(32);
      },
      verifyMessage: async ({ message, signature, address }) => {
        try {
          // 使用 viem 验证签名(推荐)
          const isValid = await verifyMessage({
            address: address as `0x${string}`,
            message,
            signature: signature as `0x${string}`,
          });
          return isValid;
        } catch (error) {
          console.error("SIWE 验证失败:", error);
          return false;
        }
      },
      ensLookup: async ({ walletAddress }) => {
        try {
          // 可选:使用 viem 查找 ENS 名称和头像
          // 你可以在这里使用 viem 的 ENS 工具
          const client = createPublicClient({
            chain: mainnet,
            transport: http(),
          });

          const ensName = await client.getEnsName({
            address: walletAddress as `0x${string}`,
          });

          const ensAvatar = ensName
            ? await client.getEnsAvatar({
                name: ensName,
              })
            : null;

          return {
            name: ensName || walletAddress,
            avatar: ensAvatar || "",
          };
        } catch {
          return {
            name: walletAddress,
            avatar: "",
          };
        }
      },
    }),
  ],
});

On this page