认证中断与响应后操作
Next.js 15.1 引入了新的认证中断 API(forbidden 和 unauthorized)以及响应后执行 API(after)。这些功能简化了认证和授权错误的处理,并允许在响应完成后执行后台任务。
认证中断 API
认证中断 API 提供了一种声明式的方式来处理认证和授权错误,它会渲染专门的错误页面而不是抛出通用错误。
启用配置
在使用 forbidden 和 unauthorized 之前,需要在 next.config.ts 中启用:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};
export default nextConfig;
forbidden - 403 禁止访问
forbidden 函数用于处理已认证用户权限不足的情况。它会渲染 forbidden.tsx 文件的内容。
基本用法
import { verifySession } from "@/lib/dal";
import { forbidden } from "next/navigation";
export default async function AdminPage() {
const session = await verifySession();
// 检查用户是否具有 'admin' 角色
if (session.role !== "admin") {
forbidden();
}
// 渲染管理员页面
return (
<main>
<h1>管理员仪表盘</h1>
<p>欢迎, {session.user.name}!</p>
</main>
);
}
自定义 403 页面
创建 forbidden.tsx 文件来自定义 403 页面的 UI:
// app/forbidden.tsx
import Link from "next/link";
export default function Forbidden() {
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold text-red-600">403 - 禁止访问</h1>
<p className="mt-4 text-gray-600">您没有权限访问此页面</p>
<Link
href="/"
className="mt-6 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
返回首页
</Link>
</main>
);
}
在 Server Action 中使用
在 Server Actions 中使用 forbidden 限制敏感操作:
"use server";
import { verifySession } from "@/lib/dal";
import { forbidden } from "next/navigation";
import { db } from "@/lib/db";
export async function updateRole(formData: FormData) {
const session = await verifySession();
// 确保只有管理员可以更新角色
if (session.role !== "admin") {
forbidden();
}
// 执行角色更新
const userId = formData.get("userId") as string;
const newRole = formData.get("role") as string;
await db.user.update({
where: { id: userId },
data: { role: newRole },
});
}
在 Route Handler 中使用
在 API 路由中使用 forbidden 限制访问:
// app/api/admin/users/route.ts
import { verifySession } from "@/lib/dal";
import { forbidden } from "next/navigation";
import { NextResponse } from "next/server";
export async function GET() {
const session = await verifySession();
if (session.role !== "admin") {
forbidden();
}
const users = await db.user.findMany();
return NextResponse.json(users);
}
unauthorized - 401 未认证
unauthorized 函数用于处理用户未登录的情况。它会渲染 unauthorized.tsx 文件的内容。
基本用法
import { verifySession } from "@/lib/dal";
import { unauthorized } from "next/navigation";
export default async function DashboardPage() {
const session = await verifySession();
if (!session) {
unauthorized();
}
// 渲染仪表盘
return (
<main>
<h1>欢迎来到仪表盘</h1>
<p>你好, {session.user.name}。</p>
</main>
);
}
自定义 401 页面
创建 unauthorized.tsx 文件来自定义 401 页面,通常包含登录表单:
// app/unauthorized.tsx
import Login from "@/components/login";
export default function Unauthorized() {
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold">401 - 未认证</h1>
<p className="mt-4 text-gray-600">请登录以访问此页面</p>
<div className="mt-6 w-full max-w-md">
<Login />
</div>
</main>
);
}
// components/login.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function Login() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
if (res.ok) {
router.refresh(); // 刷新页面以获取新会话
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
className="w-full px-3 py-2 border rounded"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
className="w-full px-3 py-2 border rounded"
required
/>
<button
type="submit"
className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
登录
</button>
</form>
);
}
在 Server Action 中使用
在 Server Actions 中使用 unauthorized 确保只有已认证用户可以执行操作:
"use server";
import { verifySession } from "@/lib/dal";
import { unauthorized } from "next/navigation";
import { db } from "@/lib/db";
export async function updateProfile(data: FormData) {
const session = await verifySession();
// 如果用户未认证,返回 401
if (!session) {
unauthorized();
}
// 执行更新
await db.user.update({
where: { id: session.userId },
data: {
name: data.get("name") as string,
email: data.get("email") as string,
},
});
}
after - 响应后操作
after 函数允许你在响应(或预渲染)完成后调度执行任务。这对于日志记录、分析和其他不应阻塞响应的副作用非常有用。
基本用法
import { after } from "next/server";
import { log } from "@/lib/analytics";
export default function Layout({ children }: { children: React.ReactNode }) {
after(() => {
// 在布局渲染并发送给用户后执行
log({
page: "dashboard",
timestamp: Date.now(),
});
});
return <>{children}</>;
}
在 Server Action 中使用
在 Server Action 中使用 after 记录用户活动:
"use server";
import { after } from "next/server";
import { cookies, headers } from "next/headers";
import { logUserAction } from "@/lib/analytics";
import { db } from "@/lib/db";
export async function createPost(formData: FormData) {
// 执行主要操作
const post = await db.post.create({
data: {
title: formData.get("title") as string,
content: formData.get("content") as string,
},
});
// 在响应后记录用户活动
after(async () => {
const userAgent = (await headers()).get("user-agent") || "unknown";
const sessionId = (await cookies()).get("session-id")?.value || "anonymous";
logUserAction({
action: "create_post",
postId: post.id,
sessionId,
userAgent,
});
});
return post;
}
在 Route Handler 中使用
在 API 路由中使用 after 进行异步日志:
import { after } from "next/server";
import { cookies, headers } from "next/headers";
import { logUserAction } from "@/lib/analytics";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
// 执行主要操作
const body = await request.json();
const result = await processData(body);
// 记录用户活动用于分析
after(async () => {
const userAgent = (await headers()).get("user-agent") || "unknown";
const sessionCookie =
(await cookies()).get("session-id")?.value || "anonymous";
logUserAction({ sessionCookie, userAgent, action: "data_process" });
});
return NextResponse.json({ status: "success", result });
}
在 Middleware 中使用
在中间件中使用 after 进行访问日志:
import { after } from "next/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { logAccess } from "@/lib/analytics";
export function middleware(request: NextRequest) {
const start = Date.now();
after(() => {
const duration = Date.now() - start;
logAccess({
path: request.nextUrl.pathname,
method: request.method,
duration,
userAgent: request.headers.get("user-agent") || "unknown",
});
});
return NextResponse.next();
}
注意事项
forbidden 和 unauthorized 的限制
不能在根布局中调用:
// ❌ 错误:不能在根布局中调用
// app/layout.tsx
export default function RootLayout({ children }) {
if (!session) {
unauthorized(); // 这会报错
}
return <html>{children}</html>;
}
可用位置:
- Server Components
- Server Actions
- Route Handlers
after 的执行保证
after 回调会在以下情况下执行:
- 响应成功发送后
- 即使响应未成功完成(如抛出错误、调用
notFound或redirect)
这意味着你可以在 after 中执行清理或日志记录,即使操作失败。
与静态页面的交互
after 不是动态 API,调用它不会使路由变为动态。如果在静态页面中使用 after,回调会在构建时执行,或每当页面重新验证时执行。
最佳实践
统一的会话验证
创建一个统一的数据访问层(DAL)来处理会话验证:
// lib/dal.ts
import { cookies } from "next/headers";
import { cache } from "react";
import { db } from "./db";
export const verifySession = cache(async () => {
const cookieStore = await cookies();
const token = cookieStore.get("session-token")?.value;
if (!token) {
return null;
}
const session = await db.session.findUnique({
where: { token },
include: { user: true },
});
return session;
});
组合使用
结合使用 unauthorized 和 forbidden 实现完整的访问控制:
import { verifySession } from "@/lib/dal";
import { unauthorized, forbidden } from "next/navigation";
export default async function AdminPage() {
const session = await verifySession();
// 第一步:检查是否已认证
if (!session) {
unauthorized();
}
// 第二步:检查是否有权限
if (session.user.role !== "admin") {
forbidden();
}
return <AdminDashboard />;
}
异步日志模式
使用 after 进行非阻塞日志记录:
import { after } from "next/server";
async function logActivity(data: LogData) {
after(async () => {
try {
await fetch("/api/logs", {
method: "POST",
body: JSON.stringify(data),
});
} catch (error) {
console.error("Failed to log activity:", error);
}
});
}
小结
本章我们学习了:
- 认证中断 API 的启用配置
forbidden函数处理权限不足的情况unauthorized函数处理未认证的情况- 自定义 403 和 401 页面
after函数在响应后执行任务- 最佳实践和注意事项
练习
- 创建一个管理员页面,使用
forbidden限制非管理员访问 - 创建一个仪表盘页面,使用
unauthorized限制未登录用户访问 - 自定义
forbidden.tsx和unauthorized.tsx页面 - 使用
after在用户操作后记录日志