如何在 Better Auth 和 Next.js 中使用 Prisma ORM
介绍
Better Auth 是一个现代化的开源 Web 应用程序身份验证解决方案。它采用 TypeScript 构建,提供简单且可扩展的身份验证体验,并支持多种数据库适配器,包括 Prisma。
在本指南中,你将把 Better Auth 连接到一个全新的 Next.js 应用程序中,并将用户持久化到 Prisma Postgres 数据库中。你可以在 GitHub 上找到本指南的完整示例。
先决条件
- Node.js 20+
- 基本熟悉 Next.js App Router 和 Prisma
1. 设置项目
创建一个新的 Next.js 应用程序
npx create-next-app@latest betterauth-nextjs-prisma
它将提示你自定义设置。选择默认值
- 你想使用 TypeScript 吗?
是 - 你想使用 ESLint 吗?
是 - 你想使用 Tailwind CSS 吗?
是 - 你的代码想放在
src/目录中吗?是 - 你想使用 App Router 吗?
是 - 你想使用 Turbopack 吗?
是 - 你想自定义导入别名吗(默认为
@/*)?否
导航到项目目录
cd betterauth-nextjs-prisma
这些选择将创建一个现代的 Next.js 项目,其中包含用于类型安全的 TypeScript、用于代码质量的 ESLint 和用于样式的 Tailwind CSS。使用 src/ 目录和 App Router 是新 Next.js 应用程序的常见约定。
2. 设置 Prisma
接下来,你将向项目添加 Prisma 以管理数据库。
2.1. 安装 Prisma 和依赖项
安装必要的 Prisma 包。依赖项略有不同,具体取决于你是否将 Prisma Postgres 与 Accelerate 或其他数据库一起使用。
npm install prisma tsx @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pg
如果你使用的是其他数据库提供程序(MySQL、SQL Server、SQLite),请安装相应的驱动程序适配器包,而不是 @prisma/adapter-pg。有关更多信息,请参阅 数据库驱动程序。
安装后,在项目中初始化 Prisma
npx prisma init --db --output ../src/generated/prisma
在设置 Prisma Postgres 数据库时,你需要回答几个问题。选择离你位置最近的区域,并为你的数据库选择一个易于记忆的名称,例如“我的 Better Auth 项目”
这将创建
- 一个包含
schema.prisma文件的prisma目录 - 一个 Prisma Postgres 数据库
- 项目根目录中包含
DATABASE_URL的.env文件 - 生成的 Prisma Client 的
output目录为better-auth/generated/prisma
2.2. 配置 Prisma
在项目根目录中创建一个 prisma.config.ts 文件,内容如下
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});
dotenv 包应该已经安装,因为它是一个 Next.js 依赖项。如果不是,请使用以下命令安装它
npm install dotenv
2.3. 生成 Prisma 客户端
运行以下命令创建数据库表并生成 Prisma 客户端
npx prisma generate
2.4. 设置全局 Prisma 客户端
在 src 目录中,创建一个 lib 文件夹,并在其中创建一个 prisma.ts 文件。此文件将用于创建和导出你的 Prisma 客户端实例。
mkdir -p src/lib
touch src/lib/prisma.ts
像这样设置 Prisma 客户端
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma =
globalForPrisma.prisma || new PrismaClient({
adapter,
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
我们建议使用连接池(如 Prisma Accelerate)来有效地管理数据库连接。
如果您选择不使用一个,请**避免**在长生命周期环境中全局实例化 PrismaClient。相反,请为每个请求创建和处置客户端,以防止耗尽数据库连接。
3. 设置 Better Auth
现在是时候集成 Better Auth 以进行身份验证了。
3.1. 安装和配置 Better Auth
首先,安装 Better Auth 核心包
npm install better-auth
接下来,生成一个安全的密钥,Better Auth 将使用它来签署身份验证令牌。这确保了你的令牌不会被篡改。
npx @better-auth/cli@latest secret
复制生成的密钥,并将其与你的应用程序 URL 一起添加到 .env 文件中
# Better Auth
BETTER_AUTH_SECRET=your-generated-secret
BETTER_AUTH_URL=https://:3000
# Prisma
DATABASE_URL="your-database-url"
现在,为 Better Auth 创建一个配置文件。在 src/lib 目录中,创建一个 auth.ts 文件
touch src/lib/auth.ts
在此文件中,你将配置 Better Auth 以使用 Prisma 适配器,该适配器允许它将用户和会话数据持久化到你的数据库中。你还将启用电子邮件和密码身份验证。
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
})
Better Auth 还支持其他登录方法,例如社交登录(Google、GitHub 等),你可以在其文档中探索。
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
},
})
如果你的应用程序运行在除 3000 之外的端口上,你必须将其添加到 auth.ts 配置中的 trustedOrigins 中,以避免身份验证请求期间出现 CORS 错误。
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
},
trustedOrigins: ['https://:3001'],
})
3.2. 将 Better Auth 模型添加到你的模式
Better Auth 提供了一个 CLI 命令,可以自动将必要的身份验证模型(User、Session、Account 和 Verification)添加到你的 schema.prisma 文件中。
运行以下命令
npx @better-auth/cli generate
它会要求确认是否覆盖你现有的 Prisma 模式。选择 y。
这将添加以下模型
model User {
id String @id
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
sessions Session[]
accounts Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@map("session")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime
updatedAt DateTime
@@map("account")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?
@@map("verification")
}
3.3. 迁移数据库
在你的模式中包含新模型后,你需要更新数据库。运行迁移以创建相应的表
npx prisma migrate dev --name add-auth-models
npx prisma generate
4. 设置 API 路由
Better Auth 需要一个 API 端点来处理身份验证请求,例如登录、注册和注销。你将在 Next.js 中创建一个全捕获 API 路由来处理发送到 /api/auth/[...all] 的所有请求。
在 src/app/api 目录中,创建一个 auth/[...all] 文件夹结构,并在其中创建一个 route.ts 文件
mkdir -p "src/app/api/auth/[...all]"
touch "src/app/api/auth/[...all]/route.ts"
将以下代码添加到新创建的 route.ts 文件中。此代码使用 Better Auth 中的助手来创建 Next.js 兼容的 GET 和 POST 请求处理程序。
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
接下来,你需要一个客户端实用程序来从你的 React 组件与这些端点交互。在 src/lib 目录中,创建一个 auth-client.ts 文件
touch src/lib/auth-client.ts
添加以下代码,它将创建你将在 UI 中使用的 React 钩子和函数
import { createAuthClient } from 'better-auth/react'
export const { signIn, signUp, signOut, useSession } = createAuthClient()
5. 设置你的页面
现在,让我们构建身份验证的用户界面。在 src/app 目录中,创建以下文件夹结构
sign-up/page.tsxsign-in/page.tsxdashboard/page.tsx
mkdir -p src/app/{sign-up,sign-in,dashboard}
touch src/app/{sign-up,sign-in,dashboard}/page.tsx
5.1. 注册页面
首先,在 src/app/sign-up/page.tsx 中创建基本的 SignUpPage 组件。这设置了页面的主容器和标题。
"use client";
export default function SignUpPage() {
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}
接下来,导入 React 和 Next.js 中必要的钩子来管理状态和导航。初始化路由器和状态变量来保存任何潜在的错误消息。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}
现在,从 Better Auth 客户端导入 signUp 函数并添加 handleSubmit 函数。此函数在表单提交时触发,并调用 Better Auth 提供的 signUp.email 方法,传入用户的姓名、电子邮件和密码。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
//add-next-lin
import { signUp } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}
为了告知用户任何问题,添加一个在 error 状态不为 null 时有条件渲染的元素。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signUp } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
{error && <p className="text-red-500">{error}</p>}
</main>
);
}
最后,添加带有用户姓名、电子邮件和密码输入字段以及提交按钮的 HTML 表单。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signUp } from "@/lib/auth-client";
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<input
name="name"
placeholder="Full Name"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
minLength={8}
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<button
type="submit"
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Create Account
</button>
</form>
</main>
);
}
5.2. 登录页面
对于登录页面,从 src/app/sign-in/page.tsx 中的基本结构开始。
"use client";
export default function SignInPage() {
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}
现在,添加状态和路由器钩子,类似于注册页面。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}
添加 handleSubmit 函数,这次导入并使用 Better Auth 中的 signIn.email 方法。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}
添加条件错误消息显示。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
{error && <p className="text-red-500">{error}</p>}
</main>
);
}
最后,添加电子邮件和密码的表单字段以及一个登录按钮。
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";
export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<button
type="submit"
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Sign In
</button>
</form>
</main>
);
}
5.3. 仪表盘页面
这是经过身份验证的用户才能访问的受保护页面。从 src/app/dashboard/page.tsx 中的基本组件开始。
"use client";
export default function DashboardPage() {
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
从你的 Better Auth 客户端导入 useSession 钩子。这个钩子是管理客户端身份验证状态的关键。它提供会话数据和挂起状态。
"use client";
import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
为了保护此路由,使用 useEffect 钩子。此效果检查会话是否已加载 (!isPending) 以及是否存在未经过身份验证的用户 (!session?.user)。如果两者都为 true,它会将用户重定向到登录页面。
"use client";
import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
import { useEffect } from "react";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
为了提供更好的用户体验,在验证会话时添加加载和重定向状态。
"use client";
import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
import { useEffect } from "react";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);
if (isPending)
return <p className="text-center mt-8 text-white">Loading...</p>;
if (!session?.user)
return <p className="text-center mt-8 text-white">Redirecting...</p>;
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}
最后,如果用户已通过身份验证,则从 session 对象显示其姓名和电子邮件。另外,导入 signOut 函数并添加一个调用它的按钮,允许用户注销。
"use client";
import { useRouter } from "next/navigation";
import { useSession, signOut } from "@/lib/auth-client";
import { useEffect } from "react";
export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();
useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);
if (isPending)
return <p className="text-center mt-8 text-white">Loading...</p>;
if (!session?.user)
return <p className="text-center mt-8 text-white">Redirecting...</p>;
const { user } = session;
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p>Welcome, {user.name || "User"}!</p>
<p>Email: {user.email}</p>
<button
onClick={() => signOut()}
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Sign Out
</button>
</main>
);
}
5.4. 主页
最后,更新主页以提供到登录和注册页面的简单导航。将 src/app/page.tsx 的内容替换为以下内容
"use client";
import { useRouter } from "next/navigation";
export default function Home() {
const router = useRouter();
return (
<main className="flex items-center justify-center h-screen bg-neutral-950 text-white">
<div className="flex gap-4">
<button
onClick={() => router.push("/sign-up")}
className="bg-white text-black font-medium px-6 py-2 rounded-md hover:bg-gray-200">
Sign Up
</button>
<button
onClick={() => router.push("/sign-in")}
className="border border-white text-white font-medium px-6 py-2 rounded-md hover:bg-neutral-800">
Sign In
</button>
</div>
</main>
);
}
6. 试用
你的应用程序现在已完全配置。
- 启动开发服务器以进行测试
npm run dev
-
在浏览器中导航到
https://:3000。你应该会看到带有“注册”和“登录”按钮的主页。 -
点击注册,创建一个新帐户,然后你将被重定向到仪表盘。然后你可以注销并重新登录。
-
要直接在数据库中查看用户数据,可以使用 Prisma Studio。
npx prisma studio
- 这将在浏览器中打开一个新标签页,你可以在其中查看
User、Session和Account表及其内容。
恭喜!你现在拥有一个使用 Better Auth、Prisma 和 Next.js 构建的完全正常运行的身份验证系统。
后续步骤
- 添加社交登录或魔术链接支持
- 实现密码重置和电子邮件验证
- 添加用户个人资料和帐户管理页面
- 部署到 Vercel 并保护你的环境变量
- 使用自定义应用程序模型扩展你的 Prisma 模式
进一步阅读
与 Prisma 保持联系
通过以下方式与我们保持联系,继续你的 Prisma 之旅: 我们的活跃社区。保持信息灵通,参与其中,并与其他开发人员协作。
- 在 X 上关注我们 获取公告、直播活动和实用技巧。
- 加入我们的 Discord 提问、与社区交流,并通过对话获得积极支持。
- 在 YouTube 上订阅 获取教程、演示和直播。
- 在 GitHub 上参与 加星收藏存储库、报告问题或为问题做出贡献。