本节摘要:中间件是"请求到达页面/接口之前"的拦截层——重定向、鉴权、地域路由、请求改写都能在这里做。本节讲清中间件的执行时机、middleware.ts 的写法、matcher 配置,以及"中间件 vs 服务器组件鉴权"的分工。
阅读完本节,你应当能够:
有些逻辑不该等页面渲染才发现:未登录访问 /dashboard 应该直接跳转登录页;访问 /old 应该301 到新地址;来自海外的请求应该走国际版。这些"请求级"决策,在页面组件里做就太晚了、太分散了。
直觉类比:中间件是"门卫"——访客(请求)进大楼(应用)前,门卫先检查:有没有预约(登录态)、走哪个门(重定向)、去哪里(路由)。检查通过才放行到具体楼层(页面/接口)。
💡 关键直觉:中间件运行在"边缘"(Edge Runtime)——在 CDN 边缘节点执行,比服务器更靠近用户,响应极快。但它运行环境受限(Node.js API 有限),所以只适合"轻量决策"(读 Cookie、重定向),不适合"重量级逻辑"(查数据库)。
// middleware.ts(项目根目录) import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { // 读取 Cookie const token = request.cookies.get("token"); if (!token && request.nextUrl.pathname.startsWith("/dashboard")) { // 未登录访问受保护页面 → 跳转登录 return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next(); // 放行 }
执行时机:请求到达 → 中间件(可重定向/改写)→ 页面/接口。
export const config = { matcher: [ "/dashboard/:path*", // /dashboard 下所有路径 "/admin/:path*", "/api/:path*", // 也拦截 API 路由 ], };
不配 matcher 会拦截所有请求(含静态资源),性能差且易出问题。始终用 matcher 精确限定范围。
| 操作 | 示例 |
|---|---|
| 重定向 | 未登录 → /login |
| 请求改写 | rewrite 到不同页面(A/B 测试) |
| 读取/设置 Cookie | 读取登录态、设置地域标记 |
| 请求头注入 | 添加自定义头传给页面 |
| 地域路由 | 按 geo 信息路由到国际版 |
| 响应头设置 | 安全头(CSP 等) |
// 地域路由示例 export function middleware(request: NextRequest) { const country = request.geo?.country ?? "US"; const url = request.nextUrl; if (country === "CN" && !url.pathname.startsWith("/cn")) { url.pathname = `/cn${url.pathname}`; return NextResponse.rewrite(url); } return NextResponse.next(); }
| 维度 | 中间件 | 服务器组件 |
|---|---|---|
| 执行位置 | 边缘(Edge) | 服务器 |
| 能力 | 轻量(Cookie/重定向) | 完整(查库/校验) |
| 适用 | 粗粒度拦截(跳转) | 细粒度权限(角色/数据级) |
| 响应方式 | 重定向/改写 | 渲染或抛错 |
最佳实践:
| 误区 | 现象 | 正解 |
|---|---|---|
| 中间件不生效 | 请求直达页面 | 检查文件位置(根目录)与 matcher |
| 登录页重定向循环 | 死循环 | matcher 排除 /login |
| 中间件用 Node API | 报错 | 只用 Edge 支持的 API |
| 静态资源被拦 | 图片/JS 加载失败 | matcher 限定动态路径 |
| 想在中间件查库 | 慢/报错 | 移到服务器组件 |
// middleware.ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { const token = request.cookies.get("token")?.value; // 1. 维护模式开关(环境变量控制) if (process.env.MAINTENANCE_MODE === "on") { const url = request.nextUrl.clone(); url.pathname = "/maintenance"; return NextResponse.rewrite(url); } // 2. 登录保护:受保护路径未登录 → 跳登录 if (!token && request.nextUrl.pathname.startsWith("/dashboard")) { const login = new URL("/login", request.url); login.searchParams.set("next", request.nextUrl.pathname); return NextResponse.redirect(login); } return NextResponse.next(); } export const config = { matcher: ["/dashboard/:path*", "/settings/:path*"], };
// app/maintenance/page.tsx export default function Maintenance() { return ( <div className="flex h-screen items-center justify-center"> <h1>系统维护中,请稍后再来</h1> </div> ); }
中间件一肩挑两件事:维护模式(改写入口)与登录保护(重定向)——请求级决策集中在入口层,页面组件专注业务。
中间件在边缘(CDN 节点)执行,比服务器更靠近用户,但只能用轻量 API。

| 场景 | 中间件做法 |
|---|---|
| 未登录拦截 | 读 Cookie → 无 token 重定向登录页 |
| 维护模式 | 读环境变量 → rewrite 到维护页 |
| 地域路由 | 读 geo → rewrite 到语言版 |
| A/B 测试 | 读 Cookie/随机 → rewrite 到变体页 |
| 安全头 | 在响应上加 CSP 等头 |
最佳实践:中间件只做"入口决策",不放业务逻辑;matcher 始终精确配置,避免拦截静态资源。
问:中间件和服务器组件的鉴权有什么区别?
中间件在边缘执行(快、轻),适合"未登录跳转"这类粗粒度判断;服务器组件在服务器执行(可查库),适合角色、数据级权限。推荐两层都做:中间件拦大门,组件查细节。
问:中间件能访问数据库吗?
不建议。中间件运行在 Edge Runtime,数据库客户端(Prisma)在 Node 环境更合适,且中间件里查库会拖慢每个请求。数据校验放服务器组件。
问:matcher 怎么写才能不拦静态资源?
只匹配动态路径:matcher: ["/dashboard/:path*", "/api/:path*"]。不写 matcher 会匹配所有请求(含静态资源),性能差。
问:中间件里设置响应头怎么做?const res = NextResponse.next(); res.headers.set("X-Header", "value"); return res;——放行前设置头。
问:中间件能读环境变量吗?
能,但注意 NEXT_PUBLIC_ 与普通变量的区别:普通环境变量在 Edge 环境同样可用(边缘函数配置里注入)。