尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Next.js 15 实战:NextAuth + MySQL 实现登录鉴权与权限控制

Next.js 15 实战:NextAuth + MySQL 实现登录鉴权与权限控制 这两年做内部管理系统我几乎每个项目都要重新捋一遍“用户怎么登录、登录态怎么保持、哪些页面需要什么角色才能进”这件事。起初图省事试过 Firebase Auth可业务表都在 MySQL 里认证数据在一个外部服务上两套数据源来回查非常别扭。后来又试过自己写 Session结果 Cookie 加解密、续期、CSRF、退出失效全得自己维护代码越写越多心里越来越没底。直到把 Next.js 15 和 NextAuth.js 组合起来才找到一套比较顺手的方案MySQL 存用户NextAuth.js 生成会话并下发 Cookie前端基本不用碰 Cookie/Session 的细节。这篇文章我会把完整过程过一遍从选型、建表、配置到登录、路由守卫、接口保护最后是部署时容易踩的坑适合正在用 Next.js 15 搭应用、需要一套完整登录鉴权方案的开发者参考。1. 为什么要折腾这套组合先想清楚认证需求再动手1.1 一套内部系统最常见的认证需求先别急着敲命令我把这类项目的认证需求盘一下。大多数后台管理、内部工具、SaaS 产品认证部分逃不开这几件事用户能用邮箱密码注册、登录登录状态要能保持一段时间刷新页面不丢某些页面比如管理后台只有特定角色能访问后端接口不能裸奔请求必须能识别当前用户用户能主动退出这些需求听起来简单但如果自己从零实现每一件都要花不少功夫。尤其是 Session 的“服务端状态”和自己下发 Cookie 的“客户端载体”怎么配合很容易在这种小项目里写成一团乱麻。1.2 为什么把 NextAuth.js 放进技术栈这套项目里最核心的选型是 NextAuth.js。我之前看过很多人纠结自己写 Session 还是用第三方认证服务。我的结论是绝不建议自己写完整会话体系也别轻易引入外部用户体系。自写 Session 的问题在于账面上的“登录”只是冰山一角。Session 怎么存、过期时间怎么续、Cookie 要不要 httpOnly、CSRF 怎么防、退出后服务端 session 和客户端 cookie 怎么同步失效这些问题一旦上线遇到真实用户每个都是潜在的坑。NextAuth.js 把这些收敛成配置项同时不绑架你的数据层和业务逻辑。相比之下Firebase Auth、Supabase Auth、Clerk 这类服务解决得也很好但它们会引入外部依赖。如果业务数据已经在 MySQL 里再让用户数据散落到外部认证服务后续做关联查询、用户画像分析会非常难受。NextAuth.js 是 Next.js 生态的事实标准认证库支持 Credentials 登录方式认证逻辑完全由自己掌控数据表也留在 MySQL 里这才是它最大的价值。1.3 为什么是 Next.js 15App Router 带来的认证范式变化Next.js 15 把cookies()、headers()、searchParams等都变成了异步 API刚升级时确实会踩坑但适应之后会发现这套模型对认证是有利的服务端组件可以直接await auth()拿 session不需要客户端状态同步middleware 可以在请求进入路由前统一做登录态校验API Route 里也能随意读取 session 做接口鉴权。认证逻辑从“登录时发一次令牌”变成“每层都能基于会话信息做决策”安全性和可维护性都提升一个档次。顺带说明一下版本NextAuth.js 目前稳定版是 v4但 App Router 全面支持的 v5 直接称为 beta。我们项目直接用了 v5下面所有配置都以 v5 为准。如果你的项目还在用 v4 配 App Router我建议认真考虑升上来v4 在 App Router 下的配置缝缝补补体验差不少。2. 从零初始化项目依赖版本与目录结构2.1 版本要求和项目创建命令Next.js 15 要求 Node.js 18.18 以上建议直接用 Node 20 LTS 或 22 LTS。创建项目我用最朴素的create-next-app交互式命令行里选择 TypeScript、App Router、Tailwind按需、ESLint 就够用npx create-next-applatest next-auth-mysql cd next-auth-mysql装依赖时注意版本。我项目里的关键依赖如下表包名版本用途next15.xReact 框架App Router 模式next-auth5.0.0-beta.x认证与 Session 管理mysql23.x连接 MySQL 的驱动支持 Promisebcryptjs2.4.x密码哈希纯 JS 实现不需要 node-gyp 编译安装命令npm install next-authbeta mysql2 bcryptjs这里有个经验bcrypt原生模块在部分 Linux 环境需要编译工具链折腾起来费时间而bcryptjs是纯 JS安全性同级别对中小项目完全够用。如果你后面要部署到 Serverless 环境bcryptjs的兼容性也会好一些。2.2 目录结构认证文件怎么摆才不乱项目初始化后我会先把认证相关文件的位置规划好。下面是这套项目最终使用的目录结构. ├── app/ │ ├── api/ │ │ ├── auth/[...nextauth]/route.ts │ │ └── register/route.ts │ ├── login/page.tsx │ ├── dashboard/page.tsx │ ├── admin/page.tsx │ └── layout.tsx ├── lib/ │ ├── db.ts │ └── auth.ts ├── types/ │ └── next-auth.d.ts ├── auth.config.ts └── middleware.ts这个结构背后有个重要原因NextAuth v5 的 middleware 跑在 Edge Runtime而 Credentials Provider 的authorize函数需要查 MySQLMySQL 驱动在 Edge Runtime 跑不了。所以官方推荐的拆分方式是把不依赖数据库的配置放在auth.config.ts把数据库相关的 provider 放在lib/auth.tsmiddleware 只引用前者服务端组件和 API 路由引用后者。一开始没做这个拆分的话middleware 一启动就报错后面我在踩坑章节还会详细说。2.3 MySQL 连接池不要用 createConnection 硬连数据库连接我用mysql2/promise的createPool连接池是必须的。HTTP 场景下每个请求都可能查库如果每次都新建连接MySQL 会很快打满而且握手流程开销很大。连接池复用连接参数调优后效果明显。// lib/db.ts import mysql from mysql2/promise const pool mysql.createPool({ host: process.env.MYSQL_HOST || localhost, port: Number(process.env.MYSQL_PORT || 3306), user: process.env.MYSQL_USER, password: process.env.MYSQL_PASSWORD, database: process.env.MYSQL_DATABASE, waitForConnections: true, connectionLimit: 10, queueLimit: 0, enableKeepAlive: true }) export default pool参数解释一下waitForConnections表示连接池满时新请求排队等待而不是直接报错connectionLimit小型应用设 10 就够enableKeepAlive保持长连接避免 MySQL 默认的wait_timeout回收连接后池里突然蹦出一堆失效连接。如果跑在 Serverless 环境连接数建议压到 1 或 2因为实例会并发启动每个实例都开 10 个连接很容易压垮数据库。这就是“为什么这么选型”的典型例子同一个配置在不同的运行环境下要跟着调。环境变量放在.env.localMYSQL_HOST127.0.0.1 MYSQL_PORT3306 MYSQL_USERroot MYSQL_PASSWORDyourpassword MYSQL_DATABASEmyapp NEXTAUTH_SECRET请生成一长串随机值NEXTAUTH_SECRET是 NextAuth 签名 session cookie 的关键密钥。开发环境下不配置也能跑但那是因为 NextAuth 每次重启自动生成临时密钥后果是每次npm run dev重启后所有旧登录状态全部失效。生产环境务必自己设置生成方式可以用openssl rand -base64 323. 用户表设计与密码存储直接决定后续安全下限3.1 建表字段别乱来这些细节很关键用户表的设计看起来简单但有几个选择影响长期使用。先看完整的建表 SQLCREATE DATABASE IF NOT EXISTS myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE myapp; CREATE TABLE users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL, name VARCHAR(100) NOT NULL DEFAULT , role VARCHAR(20) NOT NULL DEFAULT user, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_users_email (email) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个容易被忽略的细节id用BIGINT UNSIGNED虽然现在用户量少但INT上限 21 亿真心不需要为这个赌未来。MySQL 8 的 AUTO_INCREMENT 不会重用用大类型省心。role用VARCHAR(20)而不是ENUM(user,admin)ENUM 在业务扩展时修改枚举值非常痛苦要执行 ALTER 语句改表结构VARCHAR 加个判断条件就行。password_hash定 255bcrypt 输出固定 60 字符但未来可能换更强的哈希算法比如 scrypt、argon2输出更长VARCHAR(255) 余量足够。唯一索引放email登录时用邮箱查用户这步要快也杜绝了重复注册。3.2 密码哈希为什么一定用 bcrypt密码绝不能明文存。常见的哈希算法 MD5、SHA-1、SHA-256 直接查彩虹表就能撞出来几块钱就能买到的 GPU 几分钟跑完小字典。bcrypt 的设计目的是“慢”通过可调节的工作因子让每次哈希计算消耗固定CPU时间暴力破解成本指数级上升。bcryptjs用法很简单import bcrypt from bcryptjs // 注册时哈希并存储 const hash await bcrypt.hash(password, 10) // 登录时比对 const isMatch await bcrypt.compare(password, hash)工作因子 10 是性价比比较高的默认值大概在几十到一两百毫秒对用户体验无感对暴力破解已是很大障碍。不建议低于 8也不建议超过 12否则高并发登录时服务器 CPU 会先扛不住。3.3 注册接口防重复、防注入、防弱密码注册逻辑我放在一个独立的 API Route 里。边写边强调几个安全点参数校验必须在服务端做SQL 必须用预处理参数邮箱唯一性要查库确认。// app/api/register/route.ts import { NextResponse } from next/server import bcrypt from bcryptjs import pool from /lib/db export async function POST(req: Request) { const { email, password, name } await req.json() if (!email || !password || password.length 6) { return NextResponse.json({ error: 邮箱或密码格式不正确 }, { status: 400 }) } const [rows] await pool.execute( SELECT id FROM users WHERE email ?, [email] ) if ((rows as any[]).length 0) { return NextResponse.json({ error: 该邮箱已被注册 }, { status: 409 }) } const passwordHash await bcrypt.hash(password, 10) await pool.execute( INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?), [email, passwordHash, name || ] ) return NextResponse.json({ ok: true }, { status: 201 }) }注意我用的是pool.execute这会走 MySQL 预处理语句参数值不会被拼进 SQL能挡住 SQL 注入。查询和插入都用问号占位符不要用模板字符串拼接这是对抗注入的基本纪律。4. NextAuth.js 配置接受“配置多但边界清楚”的事实4.1 拆两个文件一个给 Edge一个给 Node前面目录结构里我刻意安排了auth.config.ts和lib/auth.ts。这是 NextAuth v5 在 Next.js 15 下的推荐写法核心原因是运行环境不同。auth.config.ts放不依赖数据库的配置middleware 可以直接引用// auth.config.ts import type { NextAuthConfig } from next-auth export const authConfig { pages: { signIn: /login }, session: { strategy: jwt }, callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn !!auth?.user const isOnProtectedPage nextUrl.pathname.startsWith(/dashboard) if (isOnProtectedPage) { return isLoggedIn } return true } } } satisfies NextAuthConfiglib/auth.ts是完整配置合并了 Credentials Provider// lib/auth.ts import NextAuth from next-auth import Credentials from next-auth/providers/credentials import bcrypt from bcryptjs import pool from /lib/db import { authConfig } from /auth.config export const { handlers, auth, signIn, signOut } NextAuth({ ...authConfig, providers: [ Credentials({ name: credentials, credentials: { email: { label: 邮箱, type: email }, password: { label: 密码, type: password } }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null const [rows] await pool.execute( SELECT id, email, password_hash, name, role FROM users WHERE email ?, [credentials.email] ) const user (rows as any[])[0] if (!user) return null const isMatch await bcrypt.compare(credentials.password, user.password_hash) if (!isMatch) return null return { id: String(user.id), email: user.email, name: user.name, role: user.role } } }) ], callbacks: { async jwt({ token, user }) { if (user) { token.id user.id token.role (user as { role: string }).role } return token }, async session({ session, token }) { if (session.user) { session.user.id token.id session.user.role token.role } return session } } })然后app/api/auth/[...nextauth]/route.ts只做一件事import { handlers } from /lib/auth export const { GET, POST } handlers如果项目里注册后想自动登录可以在注册成功后直接跳登录页想省这一步的话注册完调用signIn(credentials, { redirect: false, email, password })也行多一次密码校验并不亏。4.2 Cookie 和 Session先说清概念否则后面全是糊涂账标题里的“Cookie/Session 鉴权”有必要展开讲。很多人把这两个词混在一起其实它们是不同层次的东西Cookie 是浏览器端的存储载体由服务器通过Set-Cookie响应头下发。浏览器后续每个请求会自动带上同一站点的 Cookie。Session 是“会话状态”。在 NextAuth 的 JWT 策略下会话状态被编码进一个 JWT存进 Cookie 里服务端不额外保存任何会话快照在 database 策略下会话是一条数据库记录Cookie 里只放一个随机的 sessionToken服务端拿 token 去查表。所以无论哪种策略最终都是“经由 Cookie 承载会话标识”。你看到的Set-Cookie、httpOnly、SameSite、Max-Age这些属性都是这套鉴权机制的地基。明白这个再看 NextAuth 的session.strategy就非常清晰了。JWT 和 database 的取舍如下表维度JWT 策略Database 策略会话存储Cookie 内的 JWT数据库 sessions 表每次请求查库不需要需要服务端主动踢人做不到可以删 session 记录横向扩展无需共享 session 存储需要共享数据库适合场景API为主、微服务、Serverless单体应用、后台管理、需强制下线我项目走的是strategy: jwt主要原因是查库次数少、部署简单而且后台系统对“踢人下线”这种需求不敏感。但坦白说JWT 有一个容易被误解的点JWT 内容是 Base64 编码的并不是默认加密只是带签名防篡改。所以千万不要把密码、手机号这些敏感字段塞进 token放id、role、name这类不敏感信息就够了。4.3 中间件、Cookie 属性和类型扩展这些也得配到位中间件文件非常简单但它是整个路由守卫的第一道门// middleware.ts import NextAuth from next-auth import { authConfig } from /auth.config export default NextAuth(authConfig).auth export const config { matcher: [/((?!api|_next/static|_next/image|favicon.ico).*)] }matcher用正则排除掉api路径非常重要如果不排除NextAuth 自己的api/auth/*也会被中间件拦下来轻则报错重则死循环。_next/static和_next/image是 Next.js 内部资源和图片接口同样要排除。关于 Cookie 属性NextAuth 的默认值已经够安全session cookie 默认httpOnlyJS 读不到防 XSS 窃取、sameSite: lax防 CSRF、生产环境 HTTPS 下自动加secure。这些默认值不需要改也不需要去理解“为什么自己写的 session 各种坑”因为库已经在底层处理掉了。TypeScript 类型扩展是很多人漏掉的一步。因为默认的session.user只有name、email、image要加id和role必须声明模块扩展// types/next-auth.d.ts import { DefaultSession } from next-auth declare module next-auth { interface Session { user: { id: string role: string } DefaultSession[user] } interface User { role: string } } declare module next-auth/jwt { interface JWT { id: string role: string } }如果不做这步后面在中间件和页面上访问session.user.id都会得到类型报错。这是我每次都能在别人项目里看到的问题。5. 登录、路由守卫和接口鉴权把鉴权落实到每层5.1 登录页客户端调用 signIn别自己拼请求登录页用客户端组件。核心函数是 NextAuth 的signIn不要自己写 fetch 调接口因为signIn内部处理了 CSRF、回调 URL 和错误码自己拼请求容易踩坑。use client import { useState } from react import { signIn } from next-auth/react import { useRouter } from next/navigation export default function LoginPage() { const router useRouter() const [email, setEmail] useState() const [password, setPassword] useState() const [error, setError] useState() const [loading, setLoading] useState(false) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setLoading(true) setError() const result await signIn(credentials, { email, password, redirect: false }) if (result?.error) { setError(邮箱或密码错误) setLoading(false) return } router.push(/dashboard) router.refresh() } return ( form onSubmit{handleSubmit} input typeemail value{email} onChange{(e) setEmail(e.target.value)} placeholder邮箱 required / input typepassword value{password} onChange{(e) setPassword(e.target.value)} placeholder密码 required / {error p style{{ color: red }}{error}/p} button typesubmit disabled{loading} {loading ? 登录中... : 登录} /button /form ) }登录成功后router.refresh()很关键Next.js 客户端导航不会自动刷新服务端组件如果不 refresh跳转到 dashboard 后服务端组件可能拿不到最新的 session导致刚登录完页面却显示未登录。5.2 服务端组件await auth() 是日常操作受保护页面的服务端组件里直接用auth()// app/dashboard/page.tsx import { auth } from /lib/auth import { redirect } from next/navigation export default async function DashboardPage() { const session await auth() if (!session?.user) { redirect(/login) } return ( div h1欢迎回来/h1 p当前用户{session.user.name}{session.user.email}/p p角色{session.user.role}/p /div ) }这里有个值得说的点为什么中间件拦了还不够页面里还要再auth()一遍因为中间件只是第一道防线它做的是粗粒度“是否登录”的检查服务端组件里查 session 是第二道防线可以拿到完整的用户信息做细粒度的页面级权限控制。两层都做不是重复劳动而是“边界检查 数据渲染”的合理分层。5.3 中间件做角色控制admin 页面怎么拦角色控制可以放在中间件里。比如/admin前缀的页面只允许admin角色访问。这时候不能再用“直接导出已封装中间件”的方式改成一个函数// middleware.ts import NextAuth from next-auth import { NextResponse } from next/server import { authConfig } from /auth.config const { auth } NextAuth(authConfig) export default auth((req) { const { nextUrl } req const isLoggedIn !!req.auth const pathname nextUrl.pathname if (pathname /login) { if (isLoggedIn) { return NextResponse.redirect(new URL(/dashboard, nextUrl)) } return NextResponse.next() } if (pathname.startsWith(/admin)) { if (!isLoggedIn) { return NextResponse.redirect(new URL(/login, nextUrl)) } if (req.auth?.user.role ! admin) { return NextResponse.redirect(new URL(/403, nextUrl)) } } if (!isLoggedIn) { return NextResponse.redirect(new URL(/login, nextUrl)) } return NextResponse.next() }) export const config { matcher: [/((?!api|_next/static|_next/image|favicon.ico).*)] }req.auth是中间件里 NextAuth 自动解出来的 JWT 内容包含我们在jwt回调里塞进去的role所以这里判断角色不需要再查数据库。这也是 JWT 策略的一个额外红利中间件是基于签名验证和 Cookie 解析的走了 Edge Runtime 也没有数据库负担。5.4 API 接口保护别让接口裸奔前端页面能拦截但后端 API 必须自己守卫。因为任何客户端都可能绕过页面直接调接口。NextAuth v5 的auth()可以直接在 API Route 里用// app/api/profile/route.ts import { NextRequest, NextResponse } from next/server import { auth } from /lib/auth export async function GET(req: NextRequest) { const session await auth() if (!session?.user) { return NextResponse.json({ error: 未登录 }, { status: 401 }) } return NextResponse.json({ user: { id: session.user.id, email: session.user.email, name: session.user.name, role: session.user.role } }) }同样的模式可以扩展到任何需要用户身份的业务接口比如读自己的订单、改自己的信息都在查库之前先校验 session。接口里还要特别注意不能只用session.user.email当用户身份锚点应该用session.user.id因为邮箱虽然唯一但理论上可变更。5.5 退出登录服务端和客户端各有一种姿势客户端退出import { signOut } from next-auth/react await signOut({ callbackUrl: /login })服务端 Action 里退出如果要用 Server Actions 做import { signOut } from /lib/auth export async function handleSignOut() { await signOut() }注意 import 路径客户端从next-auth/react引服务端从我们封装的/lib/auth引。路径写错比如在服务端组件里用next-auth/react的signOut会直接报错。这个坑我见了太多次了。6. 实战踩坑记录这些坑不排掉照着敲也跑不通6.1 NextAuth v4 在 App Router 里的别扭体验一开始我用的是 NextAuth v4因为在 npm 上它是 stable。App Router 下跑起来问题不少v4 的getServerSession在 app 目录里用起来很绕middleware 的封装方式跟 v5 完全不同还有很多针对 Pages Router 的默认行为。后来我把依赖切成next-authbeta世界清净了。我的建议很直接只要你是 Next.js 15 App Router直接用next-authbeta。v5 虽然还挂着 beta 标签但 API 已经非常稳定官方文档也是按 v5 写的。v4 的文档在 App Router 场景下已经过时。6.2 Edge Runtime 与 MySQL 的“地域隔离”式报错这个坑我在 2.2 节埋了伏笔。刚开始我把 Credentials Provider 的authorize直接写进了auth.config.tsmiddleware 一启动就报类似 “node-mysql2 is not compatible with Edge Runtime” 的错误。原因就是next-auth的 middleware 默认跑在 Edge Runtime而mysql2依赖 Node 的 TCP 网络栈Edge 环境没有。解决办法就是拆文件。auth.config.ts只放pages、session、callbacks这些跟环境无关的配置lib/auth.ts里再合并完整 provider。middleware 引用auth.config.ts服务端页面和 API 引用lib/auth.ts。这个拆分一做好Edge 和 Node 各司其职报错直接消失。6.3 bcrypt 在 Edge 环境的另一个隐藏问题即使你把 middleware 切到auth.config.ts还有一个小坑很多人习惯在扩展authorized回调里做密码以外的逻辑判断如果在auth.config.ts里 importbcryptjs或mysql2还是一样炸。所以记住一条纪律任何与数据库、密码哈希、文件系统相关的代码都不要放进auth.config.ts它能且只能放纯逻辑和配置。6.4 NEXTAUTH_SECRET 缺失导致“每次重启都要重新登录”本地开发时如果没配NEXTAUTH_SECRETNextAuth 会用临时密钥签名 cookie。next dev重启后临时密钥变了之前签的所有 cookie 全部失效表现为“每次改完代码都要重新登录”。第一次遇到还挺懵的以为是代码写错了。解决方案就是提前在.env.local里配好NEXTAUTH_SECRET一劳永逸。生产环境更要重视密钥泄漏等于能伪造任意用户会话。6.5 MySQL 连接不上的常见原因本地 MySQL 连接失败我排过几类问题bind-address没监听0.0.0.0另一个容器或远程机器连不上用户权限范围不对root127.0.0.1不等于rootlocalhost用 TCP 连接时要给root或新用户加上合适的 hostMySQL 8 默认caching_sha2_password认证插件mysql2 是支持的但如果你的驱动太老可能报authSwitchRequest错误升级 mysql2 到 3.x 解决服务器要求 SSL 连接mysqld 配置了require_secure_transport时连接池里要加ssl: { rejectUnauthorized: false }或配置证书排查顺序建议先mysql -h 127.0.0.1 -P 3306 -u username -p用命令行确认连通性再去看应用层连接参数。命令行能连而应用连不上大概率是参数问题。6.6 部署到 Vercel 时的数据库选型如果最终部署目标是 Vercel它没有常驻本地 MySQL需要外部数据库。几个可行方案云厂商的 RDS 类服务、TiDB Cloud 的 Serverless 层、PlanetScale或者自己租一台机器跑 Docker MySQL。Serverless 环境注意把连接池connectionLimit调低否则冷启动并发实例会瞬间打爆连接数。我最终用的是一台云 MySQL 实例连接串走环境变量应用代码零改动直接部署。这一点也正好体现了这套方案“不锁定厂商”的价值换数据库只改环境变量认证逻辑完全不变。7. 最后再分享一点个人经验整套组合跑通之后最明显的感觉是“鉴权这件事终于不占脑子了”。登录、Cookie、Session 校验这些重复劳动全部交给 NextAuth 管我只需要关心业务规则哪个页面要哪种角色、哪个接口要校验什么身份。Next.js 15 的异步 API 一开始让人别扭但配合await auth()的读取方式反而比老的同步模式更符合服务端组件的直觉。如果项目刚起步我强烈建议直接上 Next.js 15 NextAuth v5 这个组合别再用 Pages Router 加手写 Session 的方式做新项目了。认证这块auth.config.ts加middleware.ts加lib/auth.ts的三层划分已经足够清晰几乎找不到理由自己从零再造一套轮子。后面我大概会把登录限流、邮箱验证、第三方 OAuth 登录也逐个加进来到时候再拆开来写一篇。
返回列表