第 14 章

Middleware:认证、重定向与 A/B 测试

第14章:Middleware:认证、重定向与 A/B 测试

Middleware 运行在 Edge Runtime,以极低延迟在请求到达页面之前执行。它是路由级别的守卫,适合认证检查、重定向、A/B 分流等快速决策逻辑。

本章核心问题:Middleware 在哪里运行?Edge Runtime 有哪些限制?如何实现 JWT 认证和 A/B 测试?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

什么是 Middleware,它在哪里运行

Next.js 的 Middleware 是一段在请求到达页面或 API 路由之前执行的代码。它运行在 Edge Runtime——一个基于 V8 isolate 的轻量级沙箱,与 Node.js 进程完全隔离。这意味着 Middleware 不能使用 fschild_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),完全不会成为性能瓶颈。

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 测试的核心需求是:同一用户每次访问看到相同变体(粘性),不同用户随机分组。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 的基础。

本章评分
4.6  / 5  (20 评分)

💬 留言讨论