以太坊登录(SIWE)
Better Auth 的以太坊登录插件
以太坊登录(Sign in with Ethereum,SIWE)插件允许用户使用其以太坊钱包按照 ERC-4361 标准进行身份验证。此插件提供灵活性,支持你自行实现消息验证与 nonce 生成逻辑。
安装
添加服务器插件
将 SIWE 插件添加到你的认证配置中:
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"
};
},
}),
],
});添加客户端插件
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
siweClient()
]
});使用方法
生成 Nonce
在签署 SIWE 消息之前,需要为钱包地址生成一个 nonce:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.nonce({
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // Ethereum 主网可选,其他链必填。默认值为 1
});
if (data) {
console.log("Nonce:", data.nonce);
}以太坊登录
生成 nonce 并创建 SIWE 消息后,验证签名以完成身份认证:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message: "Your SIWE message string",
signature: "0x...", // 用户钱包签名
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // Ethereum 主网可选,其他链必填,必须与 SIWE 消息中的链 ID 保持一致
email: "user@example.com", // 可选,anonymous 为 false 时必填
});
if (data) {
console.log("认证成功:", data.user);
}链上示例
以下为不同区块链网络的示例:
// 以太坊主网(chainId 可省略,默认为 1)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
// chainId: 1 (默认)
});// Polygon (chainId 必填)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 137, // Polygon 必填
});// Arbitrum (chainId 必填)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 42161, // Arbitrum 必填
});// Base (chainId 必填)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 8453, // Base 必填
});chainId 必须与 SIWE 消息中指定的链 ID 匹配。如果消息中的链 ID 与 chainId 参数不一致,则验证会失败,并返回 401 错误。
配置选项
服务器配置
SIWE 插件接受以下配置选项:
- domain:你的应用域名(SIWE 消息生成时必填)
- emailDomainName:创建用户账户时的邮箱域名(非匿名模式时使用)。默认取基地址域名
- anonymous:是否允许匿名登录(无需邮箱)。默认值为
true - getNonce:生成每次登录唯一 nonce 的函数。必须返回加密安全的随机字符串,返回类型为
Promise<string> - verifyMessage:验证签署的 SIWE 消息的函数。接收消息详情,需返回
Promise<boolean> - ensLookup:可选,查询以太坊地址对应的 ENS 名称和头像的函数
客户端配置
SIWE 客户端插件目前不需要任何配置选项,但如果需要,可以传入配置以便未来扩展:
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
siweClient({
// 可选的客户端配置项
}),
],
});数据表结构
SIWE 插件新增了一个 walletAddress 表,用于存储用户钱包关联信息:
| 字段 | 类型 | 描述 |
|---|---|---|
| id | string | 主键 |
| userId | string | 关联用户的 user.id |
| address | string | 以太坊钱包地址 |
| chainId | number | 链 ID(例如以太坊主网为 1) |
| isPrimary | boolean | 是否为用户的主钱包 |
| createdAt | date | 创建时间戳 |
示例实现
下面是一个完整示例,展示如何实现 SIWE 认证:
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, "a-z", "A-Z", "0-9");
},
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 名称和头像
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: "",
};
}
},
}),
],
});