Next.js 中间件实战:鉴权、重定向与 A/B 分流
Next.js 的 middleware.ts 是个特别容易被误用的东西。它跑在每一个请求进入页面之前 ,而且运行在 Edge Runtime------这意味着你不能在里面连数据库、不能用 Node 的 fs,还得小心它会拦截到静态资源。这篇用三个真实场景(登录鉴权、按地区重定向、A/B 分流)把中间件讲透,顺带把新手最常踩的 matcher 配置坑和 Edge Runtime 限制说清楚。
中间件长什么样,它在哪一层跑
在项目根目录(和 app/ 同级)建一个 middleware.ts:
typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
console.log('请求路径:', request.nextUrl.pathname)
return NextResponse.next() // 放行,继续走到目标页面
}
它跑在请求到达页面/路由之前,在 CDN 边缘节点执行(Edge Runtime),比页面渲染更早。所以它适合做「所有请求都要先过一遍」的横切逻辑:鉴权、重定向、改 header、埋点。
NextResponse.next() 表示放行;你也可以返回 redirect、rewrite 或直接一个 Response 来中断。
坑一:matcher 不配好,静态资源全被拦
新手最常见的问题:写完中间件发现图片、CSS、_next 里的 JS 全都走了一遍中间件,页面卡顿甚至白屏。因为默认情况下中间件匹配所有路径 。必须用 config.matcher 精确圈定范围:
typescript
export const config = {
matcher: [
/*
* 匹配除以下之外的所有路径:
* - _next/static (构建产物)
* - _next/image (图片优化)
* - favicon.ico、public 下的静态文件
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
这条负向前瞻的正则是官方推荐写法。没有它,中间件会白白在每个静态资源请求上跑一次,既浪费又可能出 bug。记住:matcher 是中间件性能的第一道闸。
场景一:登录鉴权与重定向
最常见的用途:未登录用户访问 /dashboard 时踢回登录页,并带上原始地址方便登录后跳回。
typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value
const { pathname } = request.nextUrl
// 受保护路由:没 token 就重定向到登录页
if (pathname.startsWith('/dashboard')) {
if (!token) {
const loginUrl = new URL('/login', request.url)
// 把用户原本想去的地址塞进 query,登录后可跳回
loginUrl.searchParams.set('from', pathname)
return NextResponse.redirect(loginUrl)
}
}
// 已登录还去登录页?直接送回 dashboard
if (pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
}
几个关键点:
request.cookies.get()是中间件里读 cookie 的方式,和页面里的cookies()不同。- 构造 URL 一定要用
new URL(path, request.url),把当前请求的 origin 带上,否则重定向会丢掉域名。 - ⚠️ 不要在中间件里验证 JWT 签名的完整业务逻辑或查数据库。Edge Runtime 没有 Node 的 crypto/db 能力,而且中间件要快。这里只做「有没有 token」这种轻量判断;真正的权限校验放到页面或 API 层。中间件是「粗筛」,不是「终审」。
场景二:按地区/语言重定向
利用中间件能读到地理信息和请求头,做多语言或多地区分流:
typescript
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 已经带语言前缀就放行,避免无限重定向
if (pathname.startsWith('/zh') || pathname.startsWith('/en')) {
return NextResponse.next()
}
// 从 Accept-Language 头粗略判断语言
const accept = request.headers.get('accept-language') || ''
const locale = accept.startsWith('zh') ? 'zh' : 'en'
// 重写(rewrite)而不是重定向:URL 不变,内部指向带语言前缀的页面
return NextResponse.rewrite(new URL(`/${locale}${pathname}`, request.url))
}
这里用了 rewrite 而不是 redirect,区别很重要:
redirect:浏览器地址栏会变,发生一次 302 跳转,用户能看到 URL 变化。rewrite:地址栏不变,服务端偷偷把请求指向另一个路径。用户以为访问的是/about,实际渲染的是/zh/about。
多语言、灰度这类「对用户透明」的场景用 rewrite;「地址确实该变」(如未登录踢到登录页)用 redirect。
场景三:A/B 分流(灰度实验)
想让 50% 用户看新版首页、50% 看旧版,且同一用户每次访问结果一致。核心是:用 cookie 记住分桶,没分桶的随机分一次并写回。
typescript
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname !== '/') return NextResponse.next()
// 读已有分桶
let bucket = request.cookies.get('ab-home')?.value
// 没分过桶:随机分配 A/B
if (bucket !== 'a' && bucket !== 'b') {
bucket = Math.random() < 0.5 ? 'a' : 'b'
}
// B 组重写到新版首页;A 组走默认
const url = bucket === 'b'
? new URL('/home-v2', request.url)
: request.nextUrl.clone()
const res = NextResponse.rewrite(url)
// 把分桶写进 cookie,保证用户后续访问稳定看到同一版本
res.cookies.set('ab-home', bucket, {
maxAge: 60 * 60 * 24 * 30, // 30 天
path: '/',
})
return res
}
export const config = {
matcher: ['/'],
}
要点:
- 分桶结果必须落 cookie 。否则每次刷新都重新
Math.random(),同一个用户一会儿 A 一会儿 B,实验数据就废了。 - 在响应对象上
res.cookies.set()写回,而不是在 request 上。中间件里 request 的 cookie 是只读视图,写要写在返回的 response 上。 - A/B 分流放在中间件层的好处:分流发生在渲染之前,用户直接拿到对应版本的页面,不会出现「先闪一下 A 再跳 B」的抖动。
Edge Runtime 的硬限制,别踩
中间件跑在 Edge Runtime,和 Node 环境不是一回事,以下这些用不了:
- Node 内置模块:
fs、net、child_process等一律不可用。 - 大部分依赖 Node API 的 npm 包(比如某些完整的数据库驱动)。
- 长时间运行的逻辑------中间件有执行时间上限,别在里面做重活。
如果发现「本地能跑、部署到 Vercel 报错」,十有八九是在中间件里用了 Edge 不支持的 API。解决思路:把重逻辑挪到 Route Handler(app/api/.../route.ts)或 Server Component,中间件只留最轻量的判断和重定向。
小结
- 中间件跑在所有页面请求之前、Edge Runtime 上,适合鉴权粗筛、重定向、A/B 分流这类横切逻辑。
config.matcher必须配 ,排除_next和静态资源,否则中间件会拖慢每一个资源请求。redirect改地址栏、rewrite不改:对用户透明的分流/多语言用 rewrite,该跳转的用 redirect。- 鉴权在中间件里只做「有没有 token」的粗筛,真正的权限校验放页面/API 层;别在 Edge Runtime 里查库或做重活。
- A/B 分流务必把分桶结果写进 cookie,并写在 response 上,保证同一用户体验一致。
一句话记忆点:中间件是请求的「安检口」------它决定谁能进、往哪走,但别把它当成处理业务的「办公室」。