本文将带你从零开始使用 Next.js 15、NextAuth v4、Drizzle ORM 和 PostgreSQL 构建一个功能完备的用户认证系统包括注册、登录、会话管理和用户信息显示。一、项目初始化与技术栈安装首先创建一个新的 Next.js 15 项目并安装必要的依赖。创建项目npx create-next-applatest my-auth-appcdmy-auth-app# 选择 TypeScript, Tailwind CSS, App Router 等选项安装核心依赖# 安装 NextAuth, Drizzle, Postgres.js, bcryptjsnpminstallnext-auth4 drizzle-orm postgres bcryptjsnpminstall-Dtypes/bcryptjs drizzle-kit二、数据库设计与Drizzle配置1. 定义数据库Schema在src/db/schema.ts中定义用户表。确保包含email和password字段。// src/db/schema.tsimport{pgTable,text,timestamp,uuid}fromdrizzle-orm/pg-core;exportconstuserspgTable(users,{id:uuid(id).defaultRandom().primaryKey(),name:text(name).notNull(),email:text(email).notNull().unique(),password:text(password).notNull(),// 用于存储加密后的密码createdAt:timestamp(created_at).defaultNow().notNull(),});exporttypeUsertypeofusers.$inferSelect;2. 配置数据库连接单例在src/db/index.ts中创建数据库连接。这能避免在 Next.js 开发环境中因热更新导致的连接数耗尽问题。// src/db/index.tsimport{drizzle}fromdrizzle-orm/postgres-js;importpostgresfrompostgres;import*asschemafrom./schema;constconnectionStringprocess.env.DATABASE_URL!;constclientpostgres(connectionString);exportconstdbdrizzle(client,{schema});3. 初始化数据库配置好.env文件中的DATABASE_URL后使用 Drizzle Kit 生成并执行迁移在 Postgres 中创建users表。npx drizzle-kit generate npx drizzle-kit migrate三、实现注册功能NextAuth 只负责认证登录不负责注册。我们需要自己创建一个 API 路由来处理用户注册和密码加密。创建注册API路由在src/app/api/auth/register/route.ts中// src/app/api/auth/register/route.tsimport{NextResponse}fromnext/server;importbcryptfrombcryptjs;import{db}from/db;import{users}from/db/schema;import{eq}fromdrizzle-orm;exportasyncfunctionPOST(request:Request){try{const{name,email,password}awaitrequest.json();// 1. 基础验证if(!name||!email||!password){returnNextResponse.json({error:所有字段均为必填},{status:400});}// 2. 检查用户是否已存在constexistingUserawaitdb.select().from(users).where(eq(users.email,email));if(existingUser.length0){returnNextResponse.json({error:邮箱已被注册},{status:400});}// 3. 加密密码consthashedPasswordawaitbcrypt.hash(password,10);// 4. 将用户信息存入数据库awaitdb.insert(users).values({name,email,password:hashedPassword,});returnNextResponse.json({message:注册成功},{status:201});}catch(error){console.error(注册错误:,error);returnNextResponse.json({error:服务器内部错误},{status:500});}}四、配置NextAuth进行登录认证1. 配置NextAuth选项在src/lib/auth.ts中配置 NextAuth使用CredentialsProvider和Drizzle来验证用户。// src/lib/auth.tsimport{NextAuthOptions}fromnext-auth;importCredentialsProviderfromnext-auth/providers/credentials;import{db}from/db;import{users}from/db/schema;import{eq}fromdrizzle-orm;importbcryptfrombcryptjs;exportconstauthOptions:NextAuthOptions{providers:[CredentialsProvider({name:credentials,credentials:{email:{label:邮箱,type:email},password:{label:密码,type:password},},asyncauthorize(credentials){if(!credentials?.email||!credentials?.password){returnnull;}// 1. 从数据库查询用户constuserawaitdb.select().from(users).where(eq(users.email,credentials.email));if(user.length0){returnnull;// 用户不存在}constdbUseruser[0];// 2. 验证密码constisPasswordValidawaitbcrypt.compare(credentials.password,dbUser.password);if(!isPasswordValid){returnnull;// 密码错误}// 3. 返回用户信息不含密码return{id:dbUser.id,email:dbUser.email,name:dbUser.name,};},}),],session:{strategy:jwt,// 使用 JWT 策略},pages:{signIn:/login,// 指定登录页面路径},callbacks:{asyncjwt({token,user}){// 将用户ID存入JWTif(user){token.iduser.id;}returntoken;},asyncsession({session,token}){// 将JWT中的ID放入sessionif(session.user){session.user.idtoken.idasstring;}returnsession;},},};2. 创建NextAuth API路由在src/app/api/auth/[...nextauth]/route.ts中// src/app/api/auth/[...nextauth]/route.tsimportNextAuthfromnext-auth;import{authOptions}from/lib/auth;consthandlerNextAuth(authOptions);export{handlerasGET,handlerasPOST};五、构建登录注册页面1. 创建登录/注册合一页面在src/app/login/page.tsx中创建一个包含登录和注册表单的页面。// src/app/login/page.tsx use client; import { useState } from react; import { signIn } from next-auth/react; import { useRouter } from next/navigation; import Link from next/link; export default function LoginPage() { const [isLogin, setIsLogin] useState(true); const [email, setEmail] useState(); const [password, setPassword] useState(); const [name, setName] useState(); const [error, setError] useState(); const [loading, setLoading] useState(false); const router useRouter(); const handleSubmit async (e: React.FormEvent) { e.preventDefault(); setError(); setLoading(true); if (isLogin) { // 登录逻辑 const result await signIn(credentials, { email, password, redirect: false, }); if (result?.error) { setError(邮箱或密码错误); } else { router.push(/dashboard); // 登录成功后跳转 router.refresh(); } } else { // 注册逻辑 const res await fetch(/api/auth/register, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name, email, password }), }); const data await res.json(); if (!res.ok) { setError(data.error); } else { alert(注册成功请登录); setIsLogin(true); } } setLoading(false); }; return ( div classNameflex min-h-screen items-center justify-center bg-gray-100 div classNamew-full max-w-md rounded-lg bg-white p-8 shadow-md h1 classNamemb-6 text-center text-2xl font-bold {isLogin ? 登录 : 注册} /h1 form onSubmit{handleSubmit} classNamespace-y-4 {!isLogin ( div label classNameblock text-sm font-medium text-gray-700姓名/label input typetext value{name} onChange{(e) setName(e.target.value)} required classNamemt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm / /div )} div label classNameblock text-sm font-medium text-gray-700邮箱/label input typeemail value{email} onChange{(e) setEmail(e.target.value)} required classNamemt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm / /div div label classNameblock text-sm font-medium text-gray-700密码/label input typepassword value{password} onChange{(e) setPassword(e.target.value)} required classNamemt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm / /div {error p classNametext-sm text-red-600{error}/p} button typesubmit disabled{loading} classNamew-full rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:bg-blue-400 {loading ? 处理中... : isLogin ? 登录 : 注册} /button /form p classNamemt-4 text-center text-sm {isLogin ? 还没有账号 : 已有账号} button onClick{() setIsLogin(!isLogin)} classNameml-1 text-blue-600 hover:underline {isLogin ? 注册 : 登录} /button /p /div /div ); }六、显示用户信息与保护路由1. 创建SessionProvider在src/components/Providers.tsx中创建SessionProvider组件用于包裹整个应用使useSession钩子可用。// src/components/Providers.tsx use client; import { SessionProvider } from next-auth/react; export function Providers({ children }: { children: React.ReactNode }) { return SessionProvider{children}/SessionProvider; }2. 在根布局中包裹Provider在src/app/layout.tsx中使用Providers组件。// src/app/layout.tsx import type { Metadata } from next; import { Providers } from /components/Providers; import ./globals.css; export const metadata: Metadata { title: NextAuth Drizzle App, description: 用户认证系统, }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( html langzh body Providers{children}/Providers /body /html ); }3. 创建受保护的仪表盘页面在src/app/dashboard/page.tsx中创建一个页面只有登录用户才能访问并显示用户信息。// src/app/dashboard/page.tsx import { getServerSession } from next-auth; import { redirect } from next/navigation; import { authOptions } from /lib/auth; export default async function DashboardPage() { const session await getServerSession(authOptions); // 如果用户未登录重定向到登录页 if (!session) { redirect(/login); } return ( div classNameflex min-h-screen items-center justify-center bg-gray-100 div classNamew-full max-w-md rounded-lg bg-white p-8 shadow-md h1 classNamemb-6 text-center text-2xl font-bold欢迎回来/h1 div classNamespace-y-2 pstrong姓名:/strong {session.user?.name}/p pstrong邮箱:/strong {session.user?.email}/p pstrong用户ID:/strong {session.user?.id}/p /div /div /div ); }七、环境变量配置在项目根目录创建.env文件并添加以下内容# 数据库连接字符串 DATABASE_URLpostgresql://user:passwordlocalhost:5432/mydb # NextAuth 密钥用于加密 JWT # 可以通过 openssl rand -base64 32 命令生成 NEXTAUTH_SECRETyour-super-secret-key-here # NextAuth URL开发环境通常是 localhost NEXTAUTH_URLhttp://localhost:3000八、await signInconst result await signIn(credentials,{email,password,redirect:false,// 阻止自动跳转我们自己控制});登录逻辑好像就只有这一段代码是怎么关联api/auth/[…nextauth]/route.ts中的接口调用的这确实是 NextAuth 最“魔法”的地方明明只写了一行signIn它背后却自动帮你跑通了整个流程。其实signIn这个函数内部封装了**“自动寻址”**的逻辑。虽然你只传了credentials这个字符串但 NextAuth 早就通过“约定”知道该去哪里找谁了。我们可以把这个过程拆解为三步“自动导航”1. 它是如何找到/api/auth/...这个地址的当你初始化 NextAuth 时在route.ts里你实际上是在告诉 NextAuth“嘿我的认证服务就部署在/api/auth/[...nextauth]这个路径下”。signIn函数默认就认准了这个标准路径。当你调用signIn(credentials, ...)时它内部会立刻拼接出一个目标 URL/api/auth/callback/credentials注意这里的callback和credentials不是随便写的而是 NextAuth 内部规定的标准动作路径。2. 它是如何知道要调用route.ts的这就回到了我们刚才讨论的[...nextauth]。前端发出的请求POST /api/auth/callback/credentialsNext.js 的路由匹配它看到api/auth/匹配上了。它看到后面跟着callback/credentials这是一串路径。它发现只有[...nextauth]这个通配符文件夹能吞下这串路径。结果Next.js 自动把请求转交给app/api/auth/[...nextauth]/route.ts里的handler处理。3. 它是如何触发authorize函数的这是最关键的一步。当请求到达route.ts后NextAuth 的handler接收到请求。它分析 URL 路径发现是/callback/credentials。它立刻明白“哦这是用户提交账号密码来验证了”。于是它去你配置的providers列表里找名为credentials的那个配置。找到后它自动执行你写在那个配置里的authorize函数。 完整的连线图你可以这样理解它们之间的“隐形连线”[前端代码] ↓ 调用 signIn(credentials, ...) ↓ [NextAuth 客户端 SDK] ↓ 自动拼接 URL - /api/auth/callback/credentials ↓ 自动带上 CSRF Token (刚才悄悄拿到的) ↓ 发起 POST 请求 ↓ [Next.js 路由系统] ↓ 发现路径匹配 [...nextauth] ↓ 转发给 app/api/auth/[...nextauth]/route.ts ↓ [NextAuth 服务端 Handler] ↓ 识别路径是 /callback/credentials ↓ 找到 CredentialsProvider 配置 ↓ 执行你写的 authorize(credentials) 函数 ↓ [数据库] ↓ 验证成功/失败之所以你觉得“代码很少”是因为NextAuth 帮你把“拼接 URL”、“发起请求”、“路由匹配”、“调用函数”这些繁琐的步骤都封装在signIn这个黑盒子里了。你只需要告诉它“我要用 credentials 方式登录”它就知道该去敲哪扇门。九、动态路由这两个问题问得非常深入这说明你已经开始透过现象看本质了。能关注到这些细节说明你离精通 NextAuth 不远了。我来逐一为你揭开这层神秘的面纱。1.[...nextauth]是什么这个写法在 Next.js 中叫做动态路由Catch-all Segments。通俗解释想象你的app/api/auth/文件夹是一个公司的前台。如果文件夹叫signin那只有/api/auth/signin这一个房间。如果文件夹叫[...nextauth]那它就是一个万能总管。它告诉 Next.js“嘿不管谁访问/api/auth/后面的什么路径不管是signin、callback、session还是signout统统都交给我这个文件来处理”为什么要这么写NextAuth 需要处理很多种请求比如POST /api/auth/signin(用户提交登录)GET /api/auth/signout(用户退出)GET /api/auth/session(前端检查用户是否登录)GET /api/auth/providers(获取支持的登录方式列表)如果不用[...nextauth]你就得在app/api/auth/下面建无数个文件夹signin,signout,session…每个里面放一个route.ts。那样太乱了结论[...nextauth]就是一个统一的入口NextAuth 内部会根据 URL 的路径比如/callback/credentials来自动判断该执行登录逻辑还是登出逻辑。你不需要改这个文件名它是 NextAuth 的标准写法。2. 点击登录时那三个接口是怎么回事这三个接口是NextAuth 客户端signIn函数自动触发的目的是为了保证安全和流程正确。我们可以把这个过程看作是一次**“特工接头”**第一阶段侦察与拿令牌接口 1/api/auth/providers什么时候触发页面加载时或者调用signIn之前。作用前端问后端“嘿你支持哪些登录方式”GitHubGoogle还是 Credentials联系NextAuth 客户端拿到这个列表确认“哦原来有 Credentials 模式”然后才继续下一步。接口 2/api/auth/csrf什么时候触发在真正提交登录表单之前。作用这是为了安全防跨站攻击 CSRF。浏览器先去拿一个“一次性暗号”CSRF Token。这个暗号会存在 Cookie 里也会返回给前端。联系如果没有这个暗号黑客就可以伪造一个假页面冒充你的用户去提交登录。NextAuth 强制要求“没有暗号我不处理登录请求”。第二阶段正式提交接口 3/api/auth/callback/credentials什么时候触发当你点击登录按钮且拿到了 CSRF Token 之后。作用这是真正的“战场”。前端把{ email, password, csrfToken }打包发给这个接口。这就是我们在route.ts里配置的authorize函数被执行的地方。验证通过后服务器会生成 Session或者 JWT并写入 Cookie。 总结一下它们的联系当你点击“登录”按钮的那一刻NextAuth 帮你自动执行了以下剧本先看一眼providers确认后端支持 Credentials 登录。拿通行证csrf先去/api/auth/csrf拿一个安全令牌防止被攻击。正式闯关callback/credentials带着你的账号密码 安全令牌去/api/auth/callback/credentials进行验证。如果验证成功 - 后端返回 Session - 页面跳转/刷新。如果验证失败 - 后端返回错误 - 前端显示“密码错误”。3. 写成[nextaut]可以吗哈哈千万别除非你想给自己挖个坑跳进去。虽然从 Next.js 的语法层面来说你把文件夹写成[nextauth]确实能跑通它依然是个动态路由但如果你这么做NextAuth 这个库本身会直接罢工。原因很简单NextAuth 的代码里写死了路径规则。 为什么不能改NextAuth 的客户端代码就是你调用的signIn函数内部默认会去请求以下路径/api/auth/signin/api/auth/session/api/auth/providers当你把文件夹命名为[...nextauth]时Next.js 会完美匹配这些路径。但如果你改成[nextauth]少那三个点它变成了一个单层动态路由。它只能匹配/api/auth/signin这一层。但是NextAuth 的回调流程比如callback/credentials是两层路径。结果就是/api/auth/signin可能还能访问但涉及到登录跳转、回调验证的时候路由匹配会直接报错404 或 500因为[nextauth]处理不了callback/credentials这种带斜杠的子路径。 简单总结[...nextauth]是Catch-all通配路由。它能吞下/api/auth/后面所有的路径不管多深。这是 NextAuth 官方规定的标准写法。[nextauth]是普通动态路由。它只能吞下一级路径。结论保留那三个点**。它是 NextAuth 能够正常工作的“暗号”。十、总结通过以上步骤你已经成功构建了一个完整的认证系统数据库使用 Drizzle 定义了用户表并处理了连接。注册创建了独立的 API 路由来处理新用户注册和密码加密。登录使用 NextAuth 的CredentialsProvider配合 Drizzle 查询数据库并验证密码。前端构建了一个登录注册合一的页面并使用signIn函数处理登录。会话通过SessionProvider和getServerSession实现了会话管理和受保护路由。用户信息在仪表盘页面成功获取并显示了当前登录用户的信息。