Middleware:认证、重定向与 A/B 测试
第14章:Middleware:认证、重定向与 A/B 测试
Middleware 运行在 Edge Runtime,以极低延迟在请求到达页面之前执行。它是路由级别的守卫,适合认证检查、重定向、A/B 分流等快速决策逻辑。
本章核心问题:Middleware 在哪里运行?Edge Runtime 有哪些限制?如何实现 JWT 认证和 A/B 测试?
读完本章你将理解:
- matcher 的精确配置与排除静态资源的重要性
- NextResponse.next()、redirect()、rewrite() 三种模式的适用场景
- Edge Runtime 的限制(无 fs、无 Node.js crypto)与应对策略
Level 1 · 你需要知道的(1-3年经验)
什么是 Middleware,它在哪里运行
Next.js 的 Middleware 是一段在请求到达页面或 API 路由之前执行的代码。它运行在 Edge Runtime——一个基于 V8 isolate 的轻量级沙箱,与 Node.js 进程完全隔离。这意味着 Middleware 不能使用 fs、child_process 或任何依赖 Node.js 原生 API 的 npm 包。
为什么要用 Edge Runtime?原因是速度。传统 Node.js 服务器在冷启动时需要几百毫秒加载运行时和模块,而 V8 isolate 的启动时间在个位数毫秒级别。Vercel、Cloudflare Workers 等边缘网络在全球数十个节点部署 Edge Runtime,让 Middleware 能在物理上最接近用户的节点执行,大幅降低首字节时间(TTFB)。
Middleware 文件必须放在项目根目录(与 app/ 同级),命名为 middleware.ts:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.next()
}
matcher:精确控制 Middleware 生效路径
默认情况下 Middleware 对所有路由生效,包括静态资源。这会造成不必要的性能损耗。matcher 配置允许你精确指定 Middleware 应当处理哪些路径:
export const config = {
matcher: [
// 匹配所有路径,排除静态文件和 Next.js 内部路由
'/((?!_next/static|_next/image|favicon.ico|public/).*)',
// 只保护 /dashboard 及其子路由
'/dashboard/:path*',
// API 路由
'/api/:path*',
],
}
matcher 支持正则语法,?、*、+ 等量词均可使用。路径参数用 :param 表示,:path* 表示零个或多个路径段。
为什么要精心设计 matcher
每次请求都经过 Middleware 会增加延迟,即便函数体什么都不做也有固定开销。更重要的是,_next/static 下的静态资源本应直接从 CDN 或文件系统返回,被 Middleware 拦截后会绕过这一优化路径。生产环境中建议始终明确排除静态资源路径。
NextResponse 的三种模式
Middleware 通过返回不同类型的 NextResponse 来控制请求流向:
NextResponse.next() — 放行请求,继续正常处理流程。可以在放行时修改请求头,将数据传递给后续的 Server Component 或 Route Handler:
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-pathname', request.nextUrl.pathname)
requestHeaders.set('x-user-country', request.geo?.country ?? 'unknown')
return NextResponse.next({
request: { headers: requestHeaders },
})
}
在 Server Component 中读取这些头:
// app/dashboard/page.tsx
import { headers } from 'next/headers'
export default async function DashboardPage() {
const headersList = await headers()
const country = headersList.get('x-user-country')
return <div>你的国家:{country}</div>
}
NextResponse.redirect() — 向客户端发送 HTTP 重定向(默认 307 临时重定向):
return NextResponse.redirect(new URL('/login', request.url))
// 或永久重定向
return NextResponse.redirect(new URL('/new-path', request.url), 301)
NextResponse.rewrite() — 在服务器端静默改写请求目标,URL 在浏览器中保持不变。常用于 A/B 测试和多租户路由:
return NextResponse.rewrite(new URL('/variant-b/page', request.url))
Level 2 · 它是怎么运行的(3-5年经验)
JWT 认证保护:完整实现
认证是 Middleware 最常见的用途。以下是一个生产级别的 JWT 验证实现,使用 Edge 兼容的 jose 库(不依赖 Node.js crypto 模块):
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET!
)
const PUBLIC_ROUTES = ['/login', '/register', '/api/auth']
async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, JWT_SECRET)
return payload
} catch {
return null
}
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 公开路由直接放行
if (PUBLIC_ROUTES.some(route => pathname.startsWith(route))) {
return NextResponse.next()
}
const token = request.cookies.get('auth-token')?.value
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
const payload = await verifyToken(token)
if (!payload) {
// Token 无效或过期,清除 cookie 并重定向
const response = NextResponse.redirect(
new URL('/login', request.url)
)
response.cookies.delete('auth-token')
return response
}
// 将用户 ID 传递给后续处理
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', String(payload.sub))
requestHeaders.set('x-user-role', String(payload.role))
return NextResponse.next({ request: { headers: requestHeaders } })
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
为什么选择 jose 而不是 jsonwebtoken
jsonwebtoken 依赖 Node.js 的 crypto 模块,在 Edge Runtime 中不可用。jose 使用 Web Crypto API,是 Edge 和浏览器环境的首选 JWT 库。验证 JWT 的计算开销极小(通常低于 1ms),完全不会成为性能瓶颈。
在 Middleware 中操作 Cookie
Cookie 是 Middleware 与客户端状态交互的主要手段。NextRequest 提供了类型安全的 cookie API:
// 读取 cookie
const theme = request.cookies.get('theme')?.value
const allCookies = request.cookies.getAll()
// 在响应上设置 cookie
const response = NextResponse.next()
response.cookies.set('theme', 'dark', {
httpOnly: false, // 客户端 JS 可读
secure: true, // 仅 HTTPS
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 365, // 1 年
path: '/',
})
// 删除 cookie
response.cookies.delete('old-session')
Level 3 · 规范怎么定义的(资深)
A/B 测试:基于 Cookie 的流量分桶
A/B 测试的核心需求是:同一用户每次访问看到相同变体(粘性),不同用户随机分组。Middleware 天然适合这个场景:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
type Variant = 'control' | 'treatment'
function assignVariant(): Variant {
return Math.random() < 0.5 ? 'control' : 'treatment'
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 只对首页进行 A/B 测试
if (pathname !== '/') {
return NextResponse.next()
}
let variant = request.cookies.get('ab-variant')?.value as Variant | undefined
// 新用户:分配变体并持久化
if (!variant || !['control', 'treatment'].includes(variant)) {
variant = assignVariant()
}
// 将流量 rewrite 到对应变体页面
// /app/ab/control/page.tsx 和 /app/ab/treatment/page.tsx
const url = request.nextUrl.clone()
url.pathname = `/ab/${variant}`
const response = NextResponse.rewrite(url)
// 持久化分配结果(30 天)
response.cookies.set('ab-variant', variant, {
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
sameSite: 'lax',
})
return response
}
变体页面的文件结构:
app/
ab/
control/
page.tsx ← 原始版本
treatment/
page.tsx ← 实验版本
page.tsx ← 实际上不会被访问(被 rewrite 拦截)
收集实验数据
在变体页面中记录曝光事件,发送到分析服务:
// app/ab/treatment/page.tsx
import { headers } from 'next/headers'
import { track } from '@/lib/analytics'
export default async function TreatmentPage() {
// 服务端记录曝光(Server Component)
await track('experiment_exposure', {
experiment: 'homepage-redesign',
variant: 'treatment',
})
return <NewHomepage />
}
基于地理位置的重定向
Vercel 等平台在 Edge Runtime 中通过请求头暴露地理位置信息,NextRequest 将其封装在 request.geo 对象中:
export function middleware(request: NextRequest) {
const country = request.geo?.country
const { pathname } = request.nextUrl
// 已经在语言特定路径则不处理
if (pathname.startsWith('/zh') || pathname.startsWith('/en')) {
return NextResponse.next()
}
if (country === 'CN') {
return NextResponse.redirect(
new URL(`/zh${pathname}`, request.url)
)
}
// 默认英文
return NextResponse.redirect(
new URL(`/en${pathname}`, request.url)
)
}
注意:request.geo 仅在部署到支持地理位置数据的平台(如 Vercel)时才有值。本地开发时为 undefined,需要做好兜底处理。
Edge Runtime 限制与应对策略
Edge Runtime 的限制源于其安全沙箱设计,以下是常见问题及解决方案:
| 限制 | 原因 | 解决方案 |
|---|---|---|
无 fs 模块 |
V8 isolate 无文件系统访问 | 将文件内容编译进代码,或通过 API 获取 |
无 crypto (Node.js) |
使用 Web Crypto API | 改用 jose、@noble/ 系列库 |
无 child_process |
沙箱限制 | 不适合在 Middleware 执行 |
| 内存限制(128MB) | 多租户共享 | 避免在 Middleware 加载大型数据结构 |
| 无持久连接 | 无状态执行 | 数据库查询应在 Server Component 中进行 |
一个经常被踩的坑:直接在 Middleware 中连接数据库验证用户。每次请求都建立数据库连接代价极高,应该改为验证 JWT(无需数据库查询)或调用轻量级 KV 存储(如 Upstash Redis)。
多层 Middleware 的组合模式
随着业务复杂度增长,Middleware 的职责会膨胀。推荐将不同功能拆分成独立函数,在主 Middleware 中按顺序组合:
// middleware.ts
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { withAuth } from './middlewares/auth'
import { withI18n } from './middlewares/i18n'
import { withRateLimit } from './middlewares/rateLimit'
type MiddlewareFn = (
request: NextRequest,
next: () => Promise<NextResponse>
) => Promise<NextResponse>
function chain(middlewares: MiddlewareFn[]) {
return async (request: NextRequest): Promise<NextResponse> => {
let index = 0
async function next(): Promise<NextResponse> {
if (index >= middlewares.length) {
return NextResponse.next()
}
const current = middlewares[index++]
return current(request, next)
}
return next()
}
}
export const middleware = chain([
withRateLimit,
withAuth,
withI18n,
])
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
这种链式模式让每个中间件专注于单一职责,便于测试和维护。每个中间件可以选择提前返回(短路)或调用 next() 将控制权传递给下一个。
Level 4 · 边界与陷阱(所有人)
陷阱1:Middleware 默认对所有路由生效,包括静态资源——必须用 matcher 精确排除 _next/static 等路径,否则会增加不必要的延迟。
陷阱2:不要在 Middleware 中直接连接数据库——每次请求都建立连接代价极高,应使用 JWT 验证(无需数据库)或轻量级 KV 存储。
陷阱3:jsonwebtoken 依赖 Node.js crypto 模块,在 Edge Runtime 不可用——必须使用 jose 库(基于 Web Crypto API)。
小结
Middleware 是 Next.js 请求管道中最靠前的守卫,它在 Edge Runtime 上以极低延迟执行,适合处理认证检查、重定向、A/B 分流和地理位置路由这类需要在每次请求时快速决策的逻辑。关键约束是 Edge Runtime 的限制——无 Node.js API、无数据库直连——这迫使我们采用更轻量、无状态的实现方式,反而提升了系统的可扩展性。掌握 matcher 的精确配置、理解三种 NextResponse 模式的适用场景,是用好 Middleware 的基础。