🙋 如何在请求到达页面之前进行认证检查?如何根据地区自动重定向?中间件可以解决这些问题。
中间件基础#
中间件在请求完成之前运行,可以修改响应、重定向、添加请求头等。
创建中间件#
在项目根目录(或 src 目录)创建 middleware.ts:
// Next.js 15.ximport { 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 指定中间件生效的路径:
// Next.js 15.ximport { 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. 认证检查#
// Next.js 15.ximport { 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. 国际化重定向#
// Next.js 15.ximport { 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. 请求头注入#
// Next.js 15.ximport { 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. 地理位置重定向#
// Next.js 15.ximport { 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 测试#
// Next.js 15.ximport { 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. 速率限制#
// Next.js 15.ximport { 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))直接响应#
// 返回 JSONreturn NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// 返回 HTMLreturn new NextResponse('<h1>Error</h1>', { status: 500, headers: { 'content-type': 'text/html' },})Cookie 操作#
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}高级用法#
条件中间件逻辑#
// Next.js 15.ximport { 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()}读取页面数据#
中间件可以向页面传递数据:
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 }, })}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,有以下限制:
- 不能使用 Node.js API(如
fs、path) - 不能使用某些 npm 包
- 代码包大小限制(1MB)
- 只能使用 Web API
性能考虑#
// ✅ 好:只匹配需要的路径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 兼容的方案:
- Vercel KV / Redis
- PlanetScale(HTTP 模式)
- 外部 API 调用
🤔 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-