Next.js 如何返回 401 与 403 并自定义 unauthorized 和 forbidden 页面
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
在 App Router 应用里,你经常需要区分两种请求:未登录用户访问受保护页面时应返回401,已登录但权限不足的用户访问时应返回403,并且两种情况都要展示自己设计的提示页,而不是框架默认内容。Next.js 提供了unauthorized与forbidden两个函数,配合unauthorized.js/forbidden.js特殊文件来完成这件事。
先说清楚适用前提:这套能力在文档中标记为实验性功能。升级指南将forbidden、unauthorized、forbidden.js、unauthorized.js和authInterrupts列在 "Features available in canary" 之下,且必须在next.config.js中显式开启authInterrupts才能使用,next/navigation导入不会自动生效。
开启 authInterrupts 开关
这是使用这两个函数的必做步骤。在next.config.js中启用authInterrupts:
module.exports = { experimental: { authInterrupts: true, }, }TypeScript 项目使用next.config.ts时:
import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { authInterrupts: true, }, } export default nextConfig在渲染路径中调用 unauthorized 与 forbidden
unauthorized用于"未登录"场景,调用后会抛出NEXT_HTTP_ERROR_FALLBACK;401错误并终止当前路由片段的渲染,Next.js 同时注入<meta name="robots" content="noindex" />防止该页被索引;forbidden用于"已登录但权限不足"场景,抛出NEXT_HTTP_ERROR_FALLBACK;403错误,其余行为相同。两者都可以在 Server Components、Server Functions(Server Actions)和 Route Handlers 中调用,但不能在 root layout 中调用。
以 Server Component 为例,未登录返回 401:
import { verifySession } from '@/app/lib/dal' import { unauthorized } from 'next/navigation' export default async function DashboardPage() { const session = await verifySession() if (!session) { unauthorized() } return <div>Dashboard</div> }角色检查返回 403:
import { verifySession } from '@/app/lib/dal' import { forbidden } from 'next/navigation' export default async function AdminPage() { const session = await verifySession() // Check if the user has the 'admin' role if (session.role !== 'admin') { forbidden() } return ( <main> <h1>Admin Dashboard</h1> <p>Welcome, {session.user.name}!</p> </main> ) }示例中的verifySession(来自@/app/lib/dal)和db(来自@/app/lib/db)是文档示例中的会话校验与数据库辅助模块,请替换为你自己的会话校验与数据访问代码,函数签名按你的实现为准。
这两个函数的工作方式是抛出异常,TypeScript 返回类型为never,所以不需要写return unauthorized(),调用后执行即停止。在 Route Handler 中保护接口端点:
import { NextRequest, NextResponse } from 'next/server' import { verifySession } from '@/app/lib/dal' import { unauthorized } from 'next/navigation' export async function GET(req: NextRequest): Promise<NextResponse> { const session = await verifySession() if (!session) { unauthorized() } // Fetch data // ... }在 Server Action 中保护敏感变更操作同理,例如只允许 admin 更新角色:
'use server' import { verifySession } from '@/app/lib/dal' import { forbidden } from 'next/navigation' export async function updateRole(formData: FormData) { const session = await verifySession() if (session.role !== 'admin') { forbidden() } // Perform the role update for authorized users // ... }自定义 unauthorized 与 forbidden 页面
自定义 UI 通过unauthorized.js/forbidden.js特殊文件完成,这两个文件不接收任何 props。文档示例同时提供.tsx与.js版本,下文以.tsx为例。
在app目录根部放置全局文件,作为所有 401 场景的兜底 UI:
import Login from '@/app/components/Login' export default function Unauthorized() { return ( <main> <h1>401 - Unauthorized</h1> <p>Please log in to access this page.</p> <Login /> </main> ) }import Link from 'next/link' export default function Forbidden() { return ( <div> <h2>Forbidden</h2> <p>You are not authorized to access this resource.</p> <Link href="/">Return Home</Link> </div> ) }其中<Login />指向文档示例中的登录组件@/app/components/Login,替换为你项目自己的登录 UI 组件。
也可以为单个路由就近放置文件来局部覆盖 UI。例如把权限检查放在<Suspense>边界内的数据加载函数中,就在该路由旁添加unauthorized.tsx:
import Link from 'next/link' export default function Unauthorized() { return ( <main> <h1>401 - Unauthorized</h1> <p> Please <Link href="/login">sign in</Link> to view your account. </p> </main> ) }抛出异常后,它会传播到最近的unauthorized/forbidden边界并渲染对应文件。
验证结果
按文档描述,配置正确时可以得到以下可核对的结果:
- 状态码:
unauthorized.js文件约定下 Next.js 返回401状态码,forbidden.js文件约定下返回403状态码; - 页面内容:请求未登录或权限不足时,用户看到的是你自定义的
unauthorized/forbidden页面,而不是受保护的内容; - 反爬取标记:响应页面中会注入
<meta name="robots" content="noindex" />; - 失败信号:如果异常没有被框架接住(见下一节的未 await 场景),开发环境的服务端会打印
⨯ unhandledRejection: NEXT_HTTP_ERROR_FALLBACK;401(forbidden对应403),且页面不会渲染任何 unauthorized / forbidden UI——看到这个日志说明调用位置有问题。
排查与限制
检查位置必须在渲染路径上,且必须被 await。由于这两个函数靠抛异常工作,只有写在组件内部、或组件await的函数内部才会触发边界渲染。把unauthorized()放进一个未被 await 的 promise,异常会抛到无人捕获的地方,401 UI 不渲染,开发环境只剩unhandledRejection日志。文档给出的模式是把校验放在数据访问层函数里并保证await:
import { Suspense } from 'react' import { verifySession } from '@/app/lib/dal' import { unauthorized } from 'next/navigation' async function getAccount() { const session = await verifySession() if (!session) { unauthorized() } return db.accounts.findByUserId(session.userId) } async function AccountDetails() { const account = await getAccount() return <p>Signed in as {account.email}</p> } export default function AccountPage() { return ( <main> <h1>Account</h1> <Suspense fallback={<p>Loading...</p>}> <AccountDetails /> </Suspense> </main> ) }try/catch 会吞掉中断。如果调用点被包在try/catch里,异常会被你捕获,401/403 UI 不再渲染。需要让框架异常穿透时,在 catch 块开头调用unstable_rethrow(err)将其重新抛出(参考 unstable_rethrow 文档)。
流式响应开始后状态码无法再变。如果检查发生在<Suspense>边界内,响应可能已以200开始流式传输,此时状态码不再能改成 401/403,用户仍会看到自定义的 unauthorized / forbidden UI。要返回真实的 401/403 状态码,检查必须发生在响应开始流式传输之前;在使用 Cache Components 时,动态路由会先流式输出静态 shell,文档建议在proxy中执行该检查。
root layout 不可调用unauthorized/forbidden。
版本说明:unauthorized、forbidden与对应的特殊文件均自v15.1.0引入,且当前需配合实验性authInterrupts配置使用,接入生产前建议按 升级指南确认你所用 Next.js 版本包含这些 canary 特性。
更多细节可参考:unauthorized 函数、forbidden 函数、unauthorized.js 文件约定、forbidden.js 文件约定、authInterrupts 配置。
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考