Skip to content

中间件

🙋 如何在请求到达页面之前进行认证检查?如何根据地区自动重定向?中间件可以解决这些问题。

中间件基础#

中间件在请求完成之前运行,可以修改响应、重定向、添加请求头等。

创建中间件#

在项目根目录(或 src 目录)创建 middleware.ts

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
console.log('请求路径:', request.nextUrl.pathname)
// 继续处理请求
return NextResponse.next()
}

匹配路径#

使用 config.matcher 指定中间件生效的路径:

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 只对匹配的路径执行
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}

多路径匹配#

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

正则匹配#

export const config = {
matcher: [
// 匹配所有路径,排除静态文件和 API
'/((?!_next/static|_next/image|favicon.ico|api).*)',
],
}

常见用例#

1. 认证检查#

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
// 未登录用户访问受保护路由
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
// 已登录用户访问登录页
if (token && request.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
}

2. 国际化重定向#

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['zh', 'en', 'ja']
const defaultLocale = 'zh'
function getLocale(request: NextRequest): string {
// 从 Accept-Language 头获取首选语言
const acceptLanguage = request.headers.get('accept-language')
if (acceptLanguage) {
const preferred = acceptLanguage.split(',')[0].split('-')[0]
if (locales.includes(preferred)) {
return preferred
}
}
return defaultLocale
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 检查路径是否已包含语言前缀
const hasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (hasLocale) {
return NextResponse.next()
}
// 重定向到带语言前缀的路径
const locale = getLocale(request)
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url))
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico).*)'],
}

3. 请求头注入#

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 克隆请求头
const requestHeaders = new Headers(request.headers)
// 添加自定义请求头
requestHeaders.set('x-custom-header', 'my-value')
requestHeaders.set('x-request-id', crypto.randomUUID())
// 传递给页面
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
// 添加响应头
response.headers.set('x-response-time', Date.now().toString())
return response
}

4. 地理位置重定向#

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'CN'
// 根据地区重定向
if (country === 'CN' && !request.nextUrl.pathname.startsWith('/cn')) {
return NextResponse.redirect(new URL('/cn', request.url))
}
if (country === 'US' && !request.nextUrl.pathname.startsWith('/us')) {
return NextResponse.redirect(new URL('/us', request.url))
}
return NextResponse.next()
}

5. A/B 测试#

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const bucket = request.cookies.get('ab-bucket')?.value
if (bucket) {
return NextResponse.next()
}
// 随机分配 A/B 测试组
const newBucket = Math.random() < 0.5 ? 'control' : 'experiment'
const response = NextResponse.next()
response.cookies.set('ab-bucket', newBucket, {
maxAge: 60 * 60 * 24 * 30, // 30 天
})
return response
}

6. 速率限制#

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// 简单的内存存储(生产环境应使用 Redis)
const rateLimit = new Map<string, { count: number; timestamp: number }>()
export function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api')) {
return NextResponse.next()
}
const ip = request.ip || 'anonymous'
const now = Date.now()
const windowMs = 60 * 1000 // 1 分钟
const maxRequests = 100
const record = rateLimit.get(ip)
if (!record || now - record.timestamp > windowMs) {
rateLimit.set(ip, { count: 1, timestamp: now })
return NextResponse.next()
}
if (record.count >= maxRequests) {
return new NextResponse('请求过于频繁', {
status: 429,
headers: {
'Retry-After': '60',
},
})
}
record.count++
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}

响应操作#

重定向#

// 临时重定向 (307)
return NextResponse.redirect(new URL('/new-path', request.url))
// 永久重定向 (308)
return NextResponse.redirect(new URL('/new-path', request.url), 308)

重写#

// 重写 URL(用户看到的 URL 不变)
return NextResponse.rewrite(new URL('/proxy-path', request.url))

直接响应#

// 返回 JSON
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// 返回 HTML
return new NextResponse('<h1>Error</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
})
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// 设置 Cookie
response.cookies.set('theme', 'dark', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 365, // 1 年
})
// 读取 Cookie
const token = request.cookies.get('token')?.value
// 删除 Cookie
response.cookies.delete('old-cookie')
return response
}

高级用法#

条件中间件逻辑#

middleware.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// API 路由处理
if (pathname.startsWith('/api')) {
return handleApiRoutes(request)
}
// 管理后台处理
if (pathname.startsWith('/admin')) {
return handleAdminRoutes(request)
}
// 普通页面处理
return handlePageRoutes(request)
}
function handleApiRoutes(request: NextRequest) {
// API 认证检查
const apiKey = request.headers.get('x-api-key')
if (!apiKey) {
return NextResponse.json({ error: 'Missing API key' }, { status: 401 })
}
return NextResponse.next()
}
function handleAdminRoutes(request: NextRequest) {
// 管理员权限检查
const token = request.cookies.get('admin-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/admin/login', request.url))
}
return NextResponse.next()
}
function handlePageRoutes(request: NextRequest) {
// 普通页面处理
return NextResponse.next()
}

读取页面数据#

中间件可以向页面传递数据:

middleware.ts
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
// 添加数据到请求头
requestHeaders.set('x-user-id', 'user-123')
requestHeaders.set('x-feature-flags', JSON.stringify({ newUI: true }))
return NextResponse.next({
request: { headers: requestHeaders },
})
}
app/page.tsx
import { headers } from 'next/headers'
export default async function Page() {
const headersList = await headers()
const userId = headersList.get('x-user-id')
const featureFlags = JSON.parse(headersList.get('x-feature-flags') || '{}')
return <div>用户: {userId}</div>
}

注意事项#

中间件限制#

🔶 中间件运行在 Edge Runtime,有以下限制:

性能考虑#

// ✅ 好:只匹配需要的路径
export const config = {
matcher: ['/dashboard/:path*'],
}
// ❌ 差:匹配所有路径
export const config = {
matcher: ['/:path*'],
}

调试技巧#

export function middleware(request: NextRequest) {
// 开发环境日志
if (process.env.NODE_ENV === 'development') {
console.log('中间件执行:', {
path: request.nextUrl.pathname,
method: request.method,
cookies: Object.fromEntries(
request.cookies.getAll().map((c) => [c.name, c.value])
),
})
}
return NextResponse.next()
}

常见问题#

🤔 Q: 中间件可以访问数据库吗?

不能直接使用传统数据库客户端。可以使用 Edge 兼容的方案:

🤔 Q: 中间件执行多次怎么办?

确保 matcher 配置正确,排除静态资源路径。

🤔 Q: 如何在中间件中验证 JWT?

import { jwtVerify } from 'jose'
async function verifyToken(token: string) {
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
const { payload } = await jwtVerify(token, secret)
return payload
}

恭喜你完成了路由系统的学习!下一篇将进入渲染模式部分,深入理解 Server Components。

-EOF-