中间件与路由守卫
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 环境:
| 能力 | 支持 | 说明 |
|---|---|---|
fetch | ✅ | Web 标准 API |
crypto | ✅ | Web Crypto API |
Headers / Request / Response | ✅ | Web 标准 |
cookies | ✅ | request.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)
}5.2 Cookie 记住语言偏好
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
下一章讲导航与路由过渡。