中间件与路由守卫

Next.js Middleware 运行在每个请求到达页面之前,是实现认证守卫、国际化路由、A/B 测试、速率限制的关键入口。本章从 middleware.ts 的运行机制讲起,覆盖 matcher 配置、认证守卫实战、多租户路由、国际化重定向等核心场景。

1. Middleware 运行机制

1.1 什么是 Middleware

Middleware 是一个运行在Edge Runtime上的函数,在每个请求到达路由之前执行:

Browser Request
    │
    ▼
[Middleware] ← 这一层!在路由匹配之前
    │
    ├── 重定向 → /login
    ├── 重写 URL → 内部路由
    ├── 修改请求头/响应头
    ├── 返回自定义响应
    └── 放行 → 继续到路由
    │
    ▼
[Route Handler / Page]

1.2 基础结构

Middleware 必须放在项目根目录(与 app/ 同级):

// middleware.ts(项目根目录)
import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(request: NextRequest) {
  console.log('Request:', request.nextUrl.pathname)
 
  // 放行请求
  return NextResponse.next()
}
 
// 配置匹配规则
export const config = {
  matcher: [
    // 匹配所有路径,排除静态资源和 API
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

1.3 Edge Runtime 限制

Middleware 运行在 Edge Runtime(V8 Isolates),不是完整的 Node.js 环境:

能力支持说明
fetchWeb 标准 API
cryptoWeb Crypto API
Headers / Request / ResponseWeb 标准
cookiesrequest.cookies
Node.js fs无文件系统访问
Node.js child_process无子进程
数据库直连不能直接连 PostgreSQL
外部 HTTP 请求可以调用认证服务 API
Middleware 不能直接访问数据库

因为 Edge Runtime 没有 Node.js TCP 支持。如果需要查询数据库进行认证,应该通过 JWT Token 或调用认证 API。

2. Matcher 配置

2.1 基础 Matcher

export const config = {
  matcher: '/chat/:path*', // 匹配 /chat 及其所有子路径
}

2.2 多路径匹配

export const config = {
  matcher: [
    '/chat/:path*',
    '/settings/:path*',
    '/admin/:path*',
    '/api/protected/:path*',
  ],
}

2.3 正则排除模式

最常用的模式——匹配所有路径,但排除静态资源:

export const config = {
  matcher: [
    // 排除以下路径:
    // - _next/static(静态资源)
    // - _next/image(图片优化)
    // - favicon.ico
    // - 公共文件(svg、png、jpg 等)
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

2.4 条件匹配(代码内判断)

如果 matcher 无法满足复杂逻辑,可以在 middleware 函数内部判断:

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
 
  // 公开路径直接放行
  const publicPaths = ['/', '/login', '/register', '/pricing', '/blog']
  if (publicPaths.some(path => pathname === path || pathname.startsWith(path + '/'))) {
    return NextResponse.next()
  }
 
  // API 路由单独处理
  if (pathname.startsWith('/api/')) {
    return handleApiAuth(request)
  }
 
  // 其他路径需要认证
  return handlePageAuth(request)
}

3. 认证守卫实战

3.1 基于 JWT Token 的认证守卫

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
 
const PUBLIC_PATHS = new Set(['/', '/login', '/register', '/pricing', '/blog', '/api/auth'])
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
 
  // 1. 公开路径放行
  if (PUBLIC_PATHS.has(pathname) || pathname.startsWith('/api/webhook')) {
    return NextResponse.next()
  }
 
  // 2. 获取 Token
  const token = request.cookies.get('session-token')?.value
 
  if (!token) {
    // 未登录 → 重定向到登录页(保存原始路径)
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }
 
  // 3. 验证 Token
  try {
    const secret = new TextEncoder().encode(process.env.AUTH_SECRET)
    const { payload } = await jwtVerify(token, secret)
 
    // 4. 管理员路由权限检查
    if (pathname.startsWith('/admin') && payload.role !== 'admin') {
      return NextResponse.redirect(new URL('/chat', request.url))
    }
 
    // 5. 注入用户信息到请求头(供下游 Server Component 使用)
    const response = NextResponse.next()
    response.headers.set('x-user-id', payload.sub as string)
    response.headers.set('x-user-role', payload.role as string)
    return response
  } catch {
    // Token 无效 → 清除 Cookie 并重定向
    const response = NextResponse.redirect(new URL('/login', request.url))
    response.cookies.delete('session-token')
    return response
  }
}
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

3.2 与 Auth.js 集成

如果使用 Auth.js (NextAuth v5),认证守卫更简洁:

// middleware.ts
import { auth } from '@/lib/auth/config'
 
export default auth((request) => {
  const { pathname } = request.nextUrl
  const isLoggedIn = !!request.auth
 
  // 已登录用户访问登录页 → 重定向到仪表盘
  if (isLoggedIn && (pathname === '/login' || pathname === '/register')) {
    return Response.redirect(new URL('/chat', request.url))
  }
 
  // 受保护路径未登录 → 重定向到登录页
  const protectedPaths = ['/chat', '/settings', '/knowledge', '/billing', '/admin']
  if (!isLoggedIn && protectedPaths.some(p => pathname.startsWith(p))) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return Response.redirect(loginUrl)
  }
 
  // 管理员路由
  if (pathname.startsWith('/admin') && request.auth?.user?.role !== 'admin') {
    return Response.redirect(new URL('/chat', request.url))
  }
})
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api/webhook).*)'],
}

3.3 API 路由认证

function handleApiAuth(request: NextRequest) {
  const authHeader = request.headers.get('authorization')
 
  if (!authHeader?.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: 'Missing authorization header' },
      { status: 401 },
    )
  }
 
  // 验证 Bearer Token...
  return NextResponse.next()
}

4. 多租户路由

4.1 子域名多租户

// middleware.ts
export function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') ?? ''
  const subdomain = hostname.split('.')[0]
 
  // 主域名不处理
  if (subdomain === 'www' || subdomain === 'app') {
    return NextResponse.next()
  }
 
  // 子域名 → 重写 URL(内部添加 tenant 前缀)
  // team1.app.com/chat → /tenant/team1/chat(内部路由)
  const url = request.nextUrl.clone()
  url.pathname = `/tenant/${subdomain}${url.pathname}`
  return NextResponse.rewrite(url)
}

4.2 路径前缀多租户

// middleware.ts
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
 
  // /workspace/ws-123/chat → 注入 workspace 信息
  const workspaceMatch = pathname.match(/^\/workspace\/([^\/]+)/)
  if (workspaceMatch) {
    const workspaceId = workspaceMatch[1]
    const response = NextResponse.next()
    response.headers.set('x-workspace-id', workspaceId)
    return response
  }
 
  return NextResponse.next()
}

5. 国际化路由

5.1 基于 Accept-Language 自动重定向

// middleware.ts
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
 
const locales = ['zh-CN', 'en-US', 'ja-JP']
const defaultLocale = 'zh-CN'
 
function getLocale(request: NextRequest): string {
  const headers = { 'accept-language': request.headers.get('accept-language') ?? '' }
  const languages = new Negotiator({ headers }).languages()
  return match(languages, locales, defaultLocale)
}
 
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
 
  // 检查 URL 是否已包含语言前缀
  const hasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
  )
 
  if (hasLocale) return NextResponse.next()
 
  // 自动添加语言前缀
  const locale = getLocale(request)
  const url = request.nextUrl.clone()
  url.pathname = `/${locale}${pathname}`
  return NextResponse.redirect(url)
}
export function middleware(request: NextRequest) {
  // 优先使用 Cookie 中保存的语言偏好
  const cookieLocale = request.cookies.get('locale')?.value
  const locale = cookieLocale && locales.includes(cookieLocale)
    ? cookieLocale
    : getLocale(request)
 
  // ...重定向逻辑
}

6. 常用 Middleware 模式

6.1 速率限制(简易版)

const rateLimit = new Map<string, { count: number; resetTime: number }>()
 
function checkRateLimit(ip: string, limit = 100, windowMs = 60000): boolean {
  const now = Date.now()
  const record = rateLimit.get(ip)
 
  if (!record || now > record.resetTime) {
    rateLimit.set(ip, { count: 1, resetTime: now + windowMs })
    return true
  }
 
  if (record.count >= limit) return false
  record.count++
  return true
}
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
    if (!checkRateLimit(ip)) {
      return NextResponse.json(
        { error: 'Too many requests' },
        { status: 429 },
      )
    }
  }
  return NextResponse.next()
}
生产环境不要用内存速率限制

上面的 Map 存在内存中,多实例部署时不共享。生产环境应该使用 Upstash Redis 或 Vercel KV。

6.2 请求日志

export function middleware(request: NextRequest) {
  const start = Date.now()
 
  const response = NextResponse.next()
 
  // 在响应头中添加请求时长
  response.headers.set('x-request-duration', `${Date.now() - start}ms`)
  response.headers.set('x-request-path', request.nextUrl.pathname)
 
  return response
}

6.3 维护模式

export function middleware(request: NextRequest) {
  const isMaintenanceMode = process.env.MAINTENANCE_MODE === 'true'
 
  if (isMaintenanceMode && !request.nextUrl.pathname.startsWith('/maintenance')) {
    return NextResponse.rewrite(new URL('/maintenance', request.url))
  }
 
  return NextResponse.next()
}

7. Middleware 最佳实践

7.1 保持轻量

Middleware 在每个匹配的请求上运行,必须保持快速:

✅ 做:
├── JWT Token 验证(纯计算,~1ms)
├── Cookie 检查
├── URL 重写/重定向
├── 请求头设置
└── 简单条件判断

❌ 不做:
├── 数据库查询(Edge Runtime 不支持直连)
├── 复杂业务逻辑
├── 大量外部 API 调用
└── 文件系统操作

7.2 组合多个逻辑

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(request: NextRequest) {
  // 按顺序执行各个中间件逻辑
  let response: NextResponse | Response | undefined
 
  response = handleMaintenance(request)
  if (response) return response
 
  response = handleI18n(request)
  if (response) return response
 
  response = handleAuth(request)
  if (response) return response
 
  return NextResponse.next()
}

本章小结

  • Middleware 运行在 Edge Runtime,在路由匹配前执行,适合认证、重定向、请求头修改
  • Matcher 控制 Middleware 匹配哪些路径,正则排除模式最常用
  • 认证守卫:JWT 验证 + Auth.js 集成,保护 SaaS 应用的受限路径
  • 多租户:子域名重写或路径前缀 + 请求头注入
  • 国际化:Accept-Language 检测 + Cookie 偏好 + URL 前缀重定向
  • 核心原则:保持轻量(~1ms),不做数据库操作,复杂逻辑交给 Server Component

下一章讲导航与路由过渡。