本节摘要:登录态怎么保持、页面怎么保护、角色怎么区分?本节讲清 Next.js 的认证方案:Session vs JWT、NextAuth(Auth.js)的接入、中间件 + 服务器组件的双层保护、以及角色权限的落地。
阅读完本节,你应当能够:
HTTP 无状态——两次请求互不相识。认证系统要解决:登录后,后续每个请求都能识别"你是谁"。两种主流方案:
直觉类比:Session 是"会员卡 + 会员档案"(每次出示卡片,店员查档案);JWT 是"盖章通行证"(证件本身自带信息与防伪,验章即通过)。
💡 关键直觉:认证(你是谁)与授权(你能干什么)是两件事。登录态解决前者,角色/权限解决后者。Next.js 项目里两者结合:中间件管"登录没登录"(粗),组件/Server Action 管"有没有权限"(细)。
npm install next-auth
// auth.ts(项目根目录,统一配置) import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ Credentials({ credentials: { email: {}, password: {} }, async authorize(credentials) { // 查库验证用户名密码 const user = await db.user.findUnique({ where: { email: credentials.email as string }, }); if (user && user.password === credentials.password) { return { id: user.id, email: user.email, role: user.role }; } return null; // 验证失败 }, }), ], session: { strategy: "jwt" }, // 用 JWT 会话 callbacks: { jwt({ token, user }) { if (user) token.role = user.role; // 把角色写进 token return token; }, session({ session, token }) { session.user.role = token.role; // 会话里带上角色 return session; }, }, });
// app/api/auth/[...nextauth]/route.ts import { handlers } from "@/auth"; export const { GET, POST } = handlers;
"use client"; import { signIn, signOut } from "next-auth/react"; export function LoginButton() { return ( <button onClick={() => signIn("credentials", { callbackUrl: "/dashboard" })} > 登录 </button> ); }
// 服务器组件中 import { auth } from "@/auth"; export default async function Dashboard() { const session = await auth(); if (!session) return <p>请先登录</p>; return <h1>欢迎,{session.user.email}</h1>; }
// middleware.ts import { auth } from "@/auth"; export default auth((req) => { if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) { return Response.redirect(new URL("/login", req.nextUrl)); } }); export const config = { matcher: ["/dashboard/:path*", "/admin/:path*"], };
// app/admin/page.tsx —— 角色校验 import { auth } from "@/auth"; import { redirect } from "next/navigation"; export default async function AdminPage() { const session = await auth(); if (session?.user?.role !== "admin") { redirect("/dashboard"); // 非管理员拦截 } return <h1>管理后台</h1>; }
"use server"; import { auth } from "@/auth"; export async function deletePost(id: number) { const session = await auth(); if (!session) return { error: "未登录" }; const post = await db.post.findUnique({ where: { id } }); if (post?.authorId !== session.user.id && session.user.role !== "admin") { return { error: "无权删除" }; } await db.post.delete({ where: { id } }); return { success: true }; }
重要:Server Action 必须自己鉴权——它直接暴露为网络接口,中间件拦不住直接调用。
| 方案 | 适合场景 | 备注 |
|---|---|---|
| NextAuth/Auth.js | 通用项目 | 支持 OAuth(GitHub/Google)+ 邮箱密码 |
| 自建 Session | 需要完全控制 | 数据库存会话 |
| 自建 JWT | 无状态 API | 注意密钥与过期管理 |
| Clerk/Auth0 | 快速上线 | 托管服务,省运维 |
| 误区 | 现象 | 正解 |
|---|---|---|
| 只在中间件鉴权 | 直接调 Action 绕过 | 每个 Action 内再校验 |
| 密码明文存库 | 泄露风险 | 用 bcrypt 哈希 |
| token 过期不处理 | 登录态时有时无 | 配置 jwt 过期时间 |
| 角色只在客户端判断 | 可被绕过 | 服务器组件/API 判断 |
| 忘配 NEXTAUTH_SECRET | 启动报错 | 环境变量配置密钥 |
// auth.ts —— 见上方基础配置 // 1. 环境变量 // .env.local: AUTH_SECRET=random-secret // 2. 中间件保护 // middleware.ts —— 见 3.1 双层保护示例 // 3. 登录页 app/login/page.tsx(客户端表单) "use client"; import { signIn } from "next-auth/react"; import { useState } from "react"; export default function LoginPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const res = await signIn("credentials", { email, password, redirect: false, }); if (res?.error) setError("邮箱或密码错误"); else window.location.href = "/dashboard"; } return ( <form onSubmit={handleSubmit} className="mx-auto max-w-sm space-y-4"> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="邮箱" required /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="密码" required /> {error && <p className="text-red-500">{error}</p>} <button type="submit">登录</button> </form> ); } // 4. 受保护页面 —— 服务器组件读 session(见 2.3)
完整链路:登录(表单 → signIn)→ session 建立(JWT)→ 中间件拦未登录 → 服务器组件读 session 显示用户 → Server Action 内鉴权防绕过——认证与授权层层设防。
await auth() 获取 session。import { hash, compare } from "bcryptjs"; // 注册时:哈希后存储(绝不存明文) const passwordHash = await hash(password, 10); await prisma.user.create({ data: { email, name, passwordHash } }); // 登录时:比对哈希 const match = await compare(inputPassword, user.passwordHash);
为什么哈希:数据库一旦泄露,明文密码直接暴露;哈希后无法反推原文(加盐 + 迭代成本)。绝不自己发明哈希算法,用成熟库。
防线说明:中间件拦"大门"(未登录跳转),服务器组件读 session 渲染个性化内容,Server Action 内部自己鉴权(最容易被忽视但最必要——Action 是直接暴露的网络接口)。三层缺一不可。
问:JWT 会话和数据库会话怎么选?
JWT 无状态(服务器不存会话,验签即过),适合分布式与 API;数据库会话可控性强(可强制下线),适合需要踢人功能的场景。NextAuth 默认 JWT,够用。
问:登录失败怎么防暴力破解?
限速:同一账号/IP 多次失败后锁定或增加验证码。可用 Upstash Rate Limit(边缘限速)或数据库计数实现。
问:密码重置怎么做?
标准流程:用户提交邮箱 → 生成一次性重置令牌(带过期时间)→ 发邮件 → 用户点击链接设置新密码 → 令牌失效。绝不能在邮件里发密码。
问:第三方登录(GitHub/Google)怎么接?
NextAuth 的 providers 里加 GitHub/Google provider,配置 client id/secret 即可——OAuth 流程由 NextAuth 处理,比自己实现安全得多。
问:授权(角色权限)怎么落地?
登录态(认证)与角色(授权)分开:JWT 里带 role,服务器组件/Server Action 里判断 role 是否允许操作。admin 路由配"仅管理员"检查。
认证解决"你是谁",授权解决"你能干什么"——NextAuth 管登录态(Session/JWT),中间件粗拦、组件与 Action 细校验。核心纪律:密码哈希存储、Action 内自证身份、角色判断放服务器侧。认证与授权分开设计,是权限系统的第一原则。
给项目接上 NextAuth 凭据登录:配置 auth.ts(Credentials provider + 密码哈希校验),挂载 /api/auth 路由,写登录表单,用中间件保护 dashboard 路径。验证:未登录访问跳转登录页、登录后显示用户信息、登出后恢复拦截。
进阶练习:加角色权限——用户表加 role 字段,JWT 回调写入角色,admin 路由做角色校验,一个 Server Action 里同时做"登录检查 + 角色检查"。再测一个真实场景:普通用户调用删除他人数据的 Action,确认被拒绝——权限边界用测试验证是生产级做法。