跳到主要内容

认证中断与响应后操作

Next.js 15.1 引入了新的认证中断 API(forbiddenunauthorized)以及响应后执行 API(after)。这些功能简化了认证和授权错误的处理,并允许在响应完成后执行后台任务。

认证中断 API

认证中断 API 提供了一种声明式的方式来处理认证和授权错误,它会渲染专门的错误页面而不是抛出通用错误。

启用配置

在使用 forbiddenunauthorized 之前,需要在 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 回调会在以下情况下执行:

  • 响应成功发送后
  • 即使响应未成功完成(如抛出错误、调用 notFoundredirect

这意味着你可以在 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;
});

组合使用

结合使用 unauthorizedforbidden 实现完整的访问控制:

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);
}
});
}

小结

本章我们学习了:

  1. 认证中断 API 的启用配置
  2. forbidden 函数处理权限不足的情况
  3. unauthorized 函数处理未认证的情况
  4. 自定义 403 和 401 页面
  5. after 函数在响应后执行任务
  6. 最佳实践和注意事项

练习

  1. 创建一个管理员页面,使用 forbidden 限制非管理员访问
  2. 创建一个仪表盘页面,使用 unauthorized 限制未登录用户访问
  3. 自定义 forbidden.tsxunauthorized.tsx 页面
  4. 使用 after 在用户操作后记录日志