本文介绍了如何在使用Next.js和NextAuth.js的React应用中集成Solana Wallet Adapter进行用户认证,包括项目设置、依赖安装、前端和后端的实现步骤。
与某些版本的 Next.js 和 NextAuth.js 不兼容
由于最近对 Next.js 和 NextAuth.js 库的更新,本指南可能与某些版本不兼容。本指南中使用的版本如下,使用其他版本可能会导致错误。
依赖 | 版本 |
---|---|
Next.js | 13.3.0 |
NextAuth.js | 4.22.0 |
@solana/wallet-adapter-base | ^0.9.22 |
@solana/wallet-adapter-react | ^0.15.32 |
@solana/web3.js | ^1.75.0 |
Web3 钱包认证提供了一种独特的用户认证方法,允许用户控制自己的数据。这种方法消除了传统电子邮件登录的需要,并为开发者提供了一种安全的、私密的方式来在其平台上认证用户。作为开发者,将 web3 钱包整合到你的认证过程中,可以为去中心化应用创造新的机会,并创造一种更具用户赋权的体验。在本指南中,我们将使用 Solana Wallet Adapter 在你的去中心化应用程序(Web3 SSO)上认证用户!
在本指南中,你将创建一个使用 Next.js 和 NextAuth.js 的简单 React 应用程序,该程序允许你认证用户(感谢 Blocksmith Labs 提供这个很棒的 示例仓库)。具体来说,你将:
你的浏览器不支持视频标签。
要跟随本指南,你将需要以下工具:
在终端中创建一个新的项目目录:
mkdir solana-auth
cd solana-auth
NextAuth.js 是一个开源的认证和授权库,适用于 Next.js,这是一个流行的基于 React 的构建 web 应用程序的框架。它为开发者提供了一种简单安全的方式来处理 Next.js 应用程序中的用户认证和授权。NextAuth.js 通常被开发者用于轻松整合多个认证提供商,例如电子邮件/密码、Google 和 Facebook。
将 NextAuth.js 示例仓库克隆到你的项目文件夹中。在终端中输入:
git clone https://github.com/nextauthjs/next-auth-example.git .
输入以下命令来安装仓库中的依赖:
yarn
#或
npm install
我们需要添加 Solana Web3 库以及一些其他关键依赖项。Solana Wallet Adapter 是一个库,用于将 Solana 钱包集成到 web 应用程序中。它提供了一个统一的 API,用于与不同的 Solana 钱包进行交互,使构建可与任何 Solana 钱包一起使用的去中心化应用更容易。
在终端中输入:
yarn add @solana/web3.js@1 @solana-mobile/wallet-adapter-mobile @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets bs58 tweetnacl
#或
npm install @solana/web3.js@1 @solana-mobile/wallet-adapter-mobile @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets bs58 tweetnacl
除了 Solana-Web3.js 库和 Solana Wallet Adapter 依赖项,我们还添加了:
NextAuth 使用 .env
变量 NEXTAUTH_SECRET
来确保 NextAuth.js 库中客户端(浏览器)和服务器之间的通信安全。它是一个字符串,作为密钥用于加密和解密在客户端和服务器之间交换的 JSON Web Tokens (JWT)。JWT 包含用户的认证信息,例如用户 ID 和会话数据。如果没有有效的 NEXTAUTH_SECRET
,客户端将无法解密 JWT 并且无法认证用户。在此过程中保持 NEXTAUTH_SECRET
的值私密并永远不与任何人分享是至关重要的。我们将通过 .env
实现这一点。
在你选择的 IDE 中打开你的项目目录,并查找一个名为 .env.local.example
的文件。将其重命名为 .env.local
,并用以下内容替换其内容:
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=ENTER_A_SECRET_KEY
你可以为密钥使用任何字符串——通过在终端中输入 openssl rand -hex 32
可以生成安全密钥(或者你也可以使用 solana-keygen grind --starts-with x:1
并使用返回的公钥值)。确保将 NEXTAUTH_SECRET
更新为该值。
在你的主项目目录中,创建一个 utils 文件夹:
mkdir utils
并在该目录中创建一个新文件 SigninMessage.ts
:
cd utils
echo > SigninMessage.ts
在 SigninMessage.ts
中粘贴以下代码:
import bs58 from "bs58";
import nacl from "tweetnacl";
type SignMessage = {
domain: string;
publicKey: string;
nonce: string;
statement: string;
};
export class SigninMessage {
domain: any;
publicKey: any;
nonce: any;
statement: any;
constructor({ domain, publicKey, nonce, statement }: SignMessage) {
this.domain = domain;
this.publicKey = publicKey;
this.nonce = nonce;
this.statement = statement;
}
prepare() {
return `${this.statement}${this.nonce}`;
}
async validate(signature: string) {
const msg = this.prepare();
const signatureUint8 = bs58.decode(signature);
const msgUint8 = new TextEncoder().encode(msg);
const pubKeyUint8 = bs58.decode(this.publicKey);
return nacl.sign.detached.verify(msgUint8, signatureUint8, pubKeyUint8);
}
}
这段代码定义了一个名为 "SigninMessage" 的类,用于验证消息的签名。
该类具有四个属性:
注意: nonce(一次性数字的缩写)是一个唯一值,仅生成一次,以防止重放攻击。重放攻击是一种网络攻击,其中攻击者拦截并重新发送有效的数据传输,诱使系统接受相同的数据多次。
prepare() 函数连接 statement 和 nonce 属性,并返回结果。
validate() 函数接受一个签名作为输入,使用 prepare() 函数获得消息,使用 bs58 库解码签名和 publicKey,然后使用 nacl 库验证签名的有效性,采用 nacl.sign.detached.verify 方法。该函数返回一个布尔值,指示签名是否有效。
我们将在身份验证 API 中使用此类。
要使用 Solana Wallet Adapter,我们需要向我们的应用程序添加三个包装器:
为此,打开 pages/_app.tsx
并用以下内容替换导入内容:
import { SessionProvider } from "next-auth/react";
import React, { useMemo } from "react";
import { ConnectionProvider, WalletProvider, } from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";
import type { AppProps } from "next/app";
require("@solana/wallet-adapter-react-ui/styles.css");
import "./styles.css";
并将你的 App 函数更新为:
export default function App({ Component, pageProps }: AppProps) {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [\
new PhantomWalletAdapter(),\
],
[]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<SessionProvider session={pageProps.session} refetchInterval={0}>
<Component {...pageProps} />
</SessionProvider>
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}
注意: 为了简化演示,我们仅使用了 Phantom。欢迎你添加所选择的其他适配器。
我们在这里所做的只是将我们的 SessionProvider
包装在钱包和连接包装器中。我们可以使用默认的 devnet 和公共 RPC,因为在此示例中,我们不会实际发出任何网络请求。
现在我们的钱包适配器已设置,让我们更新前端以连接我们的钱包并进行身份验证!
为了这个示例,我们将保持模板主体不变,但我们需要更新我们的 Header 以添加与我们的钱包适配器的功能。打开 components/header.tsx
。
将整个文件的内容替换为以下内容:
import Link from "next/link";
import { getCsrfToken, signIn, signOut, useSession } from "next-auth/react";
import styles from "./header.module.css";
import { useWalletModal } from "@solana/wallet-adapter-react-ui";
import { useWallet } from "@solana/wallet-adapter-react";
import { SigninMessage } from "../utils/SigninMessage";
import bs58 from "bs58";
import { useEffect } from "react";
export default function Header() {
const { data: session, status } = useSession();
const loading = status === "loading";
const wallet = useWallet();
const walletModal = useWalletModal();
const handleSignIn = async () => {
try {
if (!wallet.connected) {
walletModal.setVisible(true);
}
const csrf = await getCsrfToken();
if (!wallet.publicKey || !csrf || !wallet.signMessage) return;
const message = new SigninMessage({
domain: window.location.host,
publicKey: wallet.publicKey?.toBase58(),
statement: `Sign this message to sign in to the app.`,
nonce: csrf,
});
const data = new TextEncoder().encode(message.prepare());
const signature = await wallet.signMessage(data);
const serializedSignature = bs58.encode(signature);
signIn("credentials", {
message: JSON.stringify(message),
redirect: false,
signature: serializedSignature,
});
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (wallet.connected && status === "unauthenticated") {
handleSignIn();
}
}, [wallet.connected]);
return (
<header>
<noscript>
<style>{`.nojs-show { opacity: 1; top: 0; }`}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && loading ? styles.loading : styles.loaded
}`}
>
{!session && (
<>
<span className={styles.notSignedInText}>
你尚未登录
</span>
<span className={styles.buttonPrimary} onClick={handleSignIn}>
登录
</span>
</>
)}
{session?.user && (
<>
{session.user.image && (
<span
style={{ backgroundImage: `url('${session.user.image}')` }}
className={styles.avatar}
/>
)}
<span className={styles.signedInText}>
<small>已登录为</small>
<br />
<strong>{session.user.email ?? session.user.name}</strong>
</span>
<a
href={`/api/auth/signout`}
className={styles.button}
onClick={(e) => {
e.preventDefault();
signOut();
}}
>
登出
</a>
</>
)}
</p>
</div>
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link legacyBehavior href="/">
<a>主页</a>
</Link>
</li>
<li className={styles.navItem}>
<Link legacyBehavior href="/api/examples/protected">
<a>受保护的 API 路由</a>
</Link>
</li>
<li className={styles.navItem}>
<Link legacyBehavior href="/me">
<a>我</a>
</Link>
</li>
</ul>
</nav>
</header>
);
}
让我们逐步解析这段代码。此代码导出了一个 React 组件 Header
,它渲染一个包含导航栏的 header 元素。该组件使用来自 next-auth/react
库的 useSession
钩子获取当前会话数据和状态,同时使用来自 @solana/wallet-adapter-react
和 @solana/wallet-adapter-react-ui
库的 useWallet
和 useWalletModal
钩子与 Solana 钱包进行交互。
该组件具有一个登录按钮,单击后将启动 handleSignIn
登录过程:
getCsrfToken()
函数,该函数返回一个承诺,解决为跨站请求伪造Token(一个密码学安全的随机值)。该Token用作 nonce 以帮助防止跨站请求伪造攻击。message
,它将被编码为字节并由用户签名。signature
并与登录消息和 csrf Token一起传递给 signIn
函数。signIn
函数由 next-auth 包提供,处理服务器端的认证过程。它接收一个会话类型(在本例中为 "credentials")和一个选项对象,其中包含 message
、signature
和重定向选项(在本例中,重定向设置为 false
,这意味着用户在登录后将停留在当前页面)。太棒了,几乎完成了。让我们更新我们的 API!
到目前为止,我们已经建立了一个登录函数并将我们的应用程序连接到 Solana 钱包适配器。现在我们需要实现后端验证。我们需要更新 API 目录中的两个文件:pages/api
:
pages/api/auth/[...nextauth].ts
,允许我们定义自定义路由以进行认证和授权逻辑pages/api/examples/protected.ts
,我们将作为示例路由,以提醒用户他们是否已被认证打开 pages/api/auth/[...nextauth].ts
并用以下内容替换文件内容:
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { getCsrfToken } from "next-auth/react";
import { SigninMessage } from "../../../utils/SigninMessage";
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
const providers = [\
CredentialsProvider({\
name: "Solana",\
credentials: {\
message: {\
label: "Message",\
type: "text",\
},\
signature: {\
label: "Signature",\
type: "text",\
},\
},\
async authorize(credentials, req) {\
try {\
const signinMessage = new SigninMessage(\
JSON.parse(credentials?.message || "{}")\
);\
const nextAuthUrl = new URL(process.env.NEXTAUTH_URL);\
if (signinMessage.domain !== nextAuthUrl.host) {\
return null;\
}\
\
const csrfToken = await getCsrfToken({ req: { ...req, body: null } });\
\
if (signinMessage.nonce !== csrfToken) {\
return null;\
}\
\
const validationResult = await signinMessage.validate(\
credentials?.signature || ""\
);\
\
if (!validationResult)\
throw new Error("无法验证签名消息");\
\
return {\
id: signinMessage.publicKey,\
};\
} catch (e) {\
return null;\
}\
},\
}),\
];
const isDefaultSigninPage =
req.method === "GET" && req.query.nextauth?.includes("signin");
// 从默认登录页面隐藏 Solana 登录
if (isDefaultSigninPage) {
providers.pop();
}
return await NextAuth(req, res, {
providers,
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async session({ session, token }) {
// @ts-ignore
session.publicKey = token.sub;
if (session.user) {
session.user.name = token.sub;
session.user.image = `https://ui-avatars.com/api/?name=${token.sub}&background=random`;
}
return session;
},
},
});
}
让我们逐步分析代码。
next-auth
的 CredentialsProvider(你可以使用其他提供商,例如 Google、Facebook 等,只需对这段代码进行一些修改即可)。message
和 signature
字段。credentials.message
创建新的 SigninMessage 对象domain
是否与 NextAuth URL 的域匹配getCsrfToken
函数生成的 nonce 匹配message
的签名/me
页面。我们现在只需要创建一个仅授权用户才能查看的受保护页面。让我们修改默认的 pages/api/examples/protected.ts
。打开文件并用以下内容替换其内容:
import type { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const token = await getToken({ req, secret });
if (!token || !token.sub)
return res.send({
error: "用户钱包未认证",
});
if (token) {
return res.send({
content:
"这是受保护的内容。你可以访问该内容,因为你已使用 Solana 钱包登录。",
});
}
res.send({
error: "你必须使用 Solana 钱包登录才能查看此页面上的受保护内容。",
});
}
这是一个 Next.js API 端点,用于保护特定页面或资源,只有经过身份验证的用户才能访问。
next-auth/jwt
包中导入 getToken
函数并用于从请求中提取 JWT。process.env.NEXTAUTH_SECRET
环境变量读取。NEXTAUTH_SECRET
用于对在客户端和服务器之间发送的 JWT 进行签名和验证。客户端使用此密钥签署Token,服务器使用相同的密钥进行验证。保持密钥私密至关重要,因为如果攻击者获取到它,他们可以用它伪造 JWT Token并冒充任何用户。哇!这真是太多内容了。我们应该准备开始测试了。
好了,让我们开始吧!打开你的终端,输入:
yarn dev
你应该会看到如下页面:
点击 受保护的 API 路由。你应该会看到一个错误:
{"error":"用户钱包未认证"}
返回主页,然后点击 登录。你将被提示连接钱包并签名消息。
现在再试一次——点击 受保护的 API 路由。你应该会看到一条受保护的消息:
{"content":"这是受保护的内容。你可以访问该内容,因为你已使用 Solana 钱包登录。"}
太棒了!恭喜你,做得好!
NextAuth 是一个强大的库,可以轻松为你的 Next.js 应用程序添加身份验证和授权功能。通过实现 Solana Wallet Adapter,你可以轻松认证和授权用户,保护页面和路由,甚至定制用户体验……所有这些都是使用他们的 Solana 钱包实现的!
想到你可能如何利用这项技术真是令人兴奋。我们非常想听听你的想法。在 Discord 上和我们分享你的项目,或者在 Twitter 上关注我们,及时了解最新信息。
如果你对本指南有任何反馈,请告诉我们。我们很想听到你的声音。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!