Next.js 中间件
中间件是 Next.js 的强大功能,允许你在请求完成之前运行代码。本章将详细介绍如何使用中间件处理认证、重定向、国际化等常见场景。
中间件基础
创建中间件
在项目根目录创建 middleware.ts 文件:
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 中间件逻辑
console.log('中间件执行:', request.nextUrl.pathname)
// 继续处理请求
return NextResponse.next()
}
// 配置匹配路径
export const config = {
matcher: [
/*
* 匹配所有路径除了:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}基本响应类型
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. 继续处理请求
if (pathname === '/continue') {
return NextResponse.next()
}
// 2. 重定向
if (pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url))
}
// 3. 重写 URL
if (pathname === '/rewrite') {
return NextResponse.rewrite(new URL('/internal-page', request.url))
}
// 4. 返回响应
if (pathname === '/api/blocked') {
return new NextResponse('Access Denied', { status: 403 })
}
return NextResponse.next()
}认证中间件
JWT 认证
typescript
// 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!)
async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, JWT_SECRET)
return payload
} catch (error) {
return null
}
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 需要认证的路径
const protectedPaths = ['/dashboard', '/profile', '/admin']
const isProtectedPath = protectedPaths.some(path =>
pathname.startsWith(path)
)
if (isProtectedPath) {
const token = request.cookies.get('token')?.value
if (!token) {
// 重定向到登录页
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}
const user = await verifyToken(token)
if (!user) {
// Token 无效,清除 cookie 并重定向
const response = NextResponse.redirect(new URL('/login', request.url))
response.cookies.delete('token')
return response
}
// 将用户信息添加到请求头
const response = NextResponse.next()
response.headers.set('x-user-id', user.sub as string)
response.headers.set('x-user-role', user.role as string)
return response
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/admin/:path*']
}基于角色的访问控制
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
interface User {
id: string
role: 'admin' | 'user' | 'moderator'
}
async function getUserFromToken(token: string): Promise<User | null> {
// 实现 token 验证逻辑
try {
const response = await fetch(`${process.env.API_URL}/auth/verify`, {
headers: { Authorization: `Bearer ${token}` }
})
if (response.ok) {
return await response.json()
}
} catch (error) {
console.error('Token verification failed:', error)
}
return null
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 定义路径权限
const pathPermissions = {
'/admin': ['admin'],
'/moderator': ['admin', 'moderator'],
'/dashboard': ['admin', 'moderator', 'user'],
}
const requiredRoles = Object.entries(pathPermissions).find(([path]) =>
pathname.startsWith(path)
)?.[1]
if (requiredRoles) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
const user = await getUserFromToken(token)
if (!user || !requiredRoles.includes(user.role)) {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// 添加用户信息到请求头
const response = NextResponse.next()
response.headers.set('x-user-id', user.id)
response.headers.set('x-user-role', user.role)
return response
}
return NextResponse.next()
}国际化中间件
语言检测和重定向
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['en', 'zh', 'ja', 'ko']
const defaultLocale = 'en'
function getLocale(request: NextRequest): string {
// 1. 检查 URL 路径中的语言
const pathname = request.nextUrl.pathname
const pathnameLocale = locales.find(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameLocale) return pathnameLocale
// 2. 检查 cookie
const cookieLocale = request.cookies.get('locale')?.value
if (cookieLocale && locales.includes(cookieLocale)) {
return cookieLocale
}
// 3. 检查 Accept-Language 头
const acceptLanguage = request.headers.get('accept-language')
if (acceptLanguage) {
const browserLocale = acceptLanguage
.split(',')[0]
.split('-')[0]
if (locales.includes(browserLocale)) {
return browserLocale
}
}
return defaultLocale
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 检查路径是否已包含语言前缀
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (!pathnameHasLocale) {
const locale = getLocale(request)
const newUrl = new URL(`/${locale}${pathname}`, request.url)
const response = NextResponse.redirect(newUrl)
response.cookies.set('locale', locale, { maxAge: 60 * 60 * 24 * 365 })
return response
}
return NextResponse.next()
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}语言切换
typescript
// components/LanguageSwitcher.tsx
'use client'
import { useRouter, usePathname } from 'next/navigation'
import { useState } from 'react'
const languages = {
en: 'English',
zh: '中文',
ja: '日本語',
ko: '한국어',
}
export default function LanguageSwitcher() {
const router = useRouter()
const pathname = usePathname()
const [currentLocale, setCurrentLocale] = useState('en')
const switchLanguage = (newLocale: string) => {
// 移除当前语言前缀
const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}/, '')
// 添加新语言前缀
const newPath = `/${newLocale}${pathWithoutLocale}`
// 设置 cookie
document.cookie = `locale=${newLocale}; path=/; max-age=${60 * 60 * 24 * 365}`
setCurrentLocale(newLocale)
router.push(newPath)
}
return (
<select
value={currentLocale}
onChange={(e) => switchLanguage(e.target.value)}
className="border rounded px-2 py-1"
>
{Object.entries(languages).map(([code, name]) => (
<option key={code} value={code}>
{name}
</option>
))}
</select>
)
}安全中间件
CSRF 保护
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import crypto from 'crypto'
function generateCSRFToken(): string {
return crypto.randomBytes(32).toString('hex')
}
function verifyCSRFToken(token: string, sessionToken: string): boolean {
return token === sessionToken
}
export function middleware(request: NextRequest) {
const { method, nextUrl } = request
// 只对状态改变的请求进行 CSRF 检查
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
const csrfToken = request.headers.get('x-csrf-token')
const sessionToken = request.cookies.get('csrf-token')?.value
if (!csrfToken || !sessionToken || !verifyCSRFToken(csrfToken, sessionToken)) {
return new NextResponse('CSRF token mismatch', { status: 403 })
}
}
// 为 GET 请求生成 CSRF token
if (method === 'GET' && !request.cookies.get('csrf-token')) {
const token = generateCSRFToken()
const response = NextResponse.next()
response.cookies.set('csrf-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
})
return response
}
return NextResponse.next()
}速率限制
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// 简单的内存存储(生产环境建议使用 Redis)
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
function getRateLimitKey(request: NextRequest): string {
// 使用 IP 地址作为限制键
const forwarded = request.headers.get('x-forwarded-for')
const ip = forwarded ? forwarded.split(',')[0] : request.ip || 'unknown'
return ip
}
function isRateLimited(key: string, limit: number, windowMs: number): boolean {
const now = Date.now()
const record = rateLimitMap.get(key)
if (!record || now > record.resetTime) {
// 重置或创建新记录
rateLimitMap.set(key, {
count: 1,
resetTime: now + windowMs
})
return false
}
if (record.count >= limit) {
return true
}
record.count++
return false
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 对 API 路由应用速率限制
if (pathname.startsWith('/api/')) {
const key = getRateLimitKey(request)
const limit = 100 // 每分钟 100 次请求
const windowMs = 60 * 1000 // 1 分钟
if (isRateLimited(key, limit, windowMs)) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': '60'
}
})
}
}
return NextResponse.next()
}重定向和重写
动态重定向
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// 重定向规则
const redirectRules = [
{ from: '/old-blog/:slug*', to: '/blog/:slug*' },
{ from: '/products/:id', to: '/shop/products/:id' },
{ from: '/user/:id', to: '/profile/:id' },
]
function applyRedirectRules(pathname: string): string | null {
for (const rule of redirectRules) {
const fromPattern = rule.from.replace(/:\w+\*/g, '(.*)').replace(/:\w+/g, '([^/]+)')
const regex = new RegExp(`^${fromPattern}$`)
const match = pathname.match(regex)
if (match) {
let to = rule.to
match.slice(1).forEach((param, index) => {
to = to.replace(/:\w+\*?/, param)
})
return to
}
}
return null
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 应用重定向规则
const redirectTo = applyRedirectRules(pathname)
if (redirectTo) {
return NextResponse.redirect(new URL(redirectTo, request.url))
}
return NextResponse.next()
}A/B 测试
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
function getVariant(request: NextRequest): 'A' | 'B' {
// 检查现有 cookie
const existingVariant = request.cookies.get('ab-test-variant')?.value
if (existingVariant === 'A' || existingVariant === 'B') {
return existingVariant
}
// 随机分配变体
return Math.random() < 0.5 ? 'A' : 'B'
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 只对首页进行 A/B 测试
if (pathname === '/') {
const variant = getVariant(request)
let response: NextResponse
if (variant === 'B') {
// 重写到 B 版本页面
response = NextResponse.rewrite(new URL('/home-variant-b', request.url))
} else {
response = NextResponse.next()
}
// 设置变体 cookie
response.cookies.set('ab-test-variant', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 天
})
return response
}
return NextResponse.next()
}日志和监控
请求日志
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
interface RequestLog {
timestamp: string
method: string
url: string
userAgent: string
ip: string
duration?: number
}
function logRequest(log: RequestLog) {
// 发送到日志服务
console.log(JSON.stringify(log))
// 或发送到外部服务
// fetch('/api/logs', {
// method: 'POST',
// body: JSON.stringify(log)
// })
}
export function middleware(request: NextRequest) {
const startTime = Date.now()
const log: RequestLog = {
timestamp: new Date().toISOString(),
method: request.method,
url: request.url,
userAgent: request.headers.get('user-agent') || '',
ip: request.headers.get('x-forwarded-for') || request.ip || 'unknown',
}
const response = NextResponse.next()
// 添加响应时间
response.headers.set('x-response-time', `${Date.now() - startTime}ms`)
// 异步记录日志
Promise.resolve().then(() => {
log.duration = Date.now() - startTime
logRequest(log)
})
return response
}性能监控
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const performanceMetrics = new Map<string, number[]>()
function recordMetric(path: string, duration: number) {
if (!performanceMetrics.has(path)) {
performanceMetrics.set(path, [])
}
const metrics = performanceMetrics.get(path)!
metrics.push(duration)
// 只保留最近 100 个记录
if (metrics.length > 100) {
metrics.shift()
}
}
function getAverageResponseTime(path: string): number {
const metrics = performanceMetrics.get(path)
if (!metrics || metrics.length === 0) return 0
const sum = metrics.reduce((a, b) => a + b, 0)
return sum / metrics.length
}
export function middleware(request: NextRequest) {
const startTime = Date.now()
const { pathname } = request.nextUrl
const response = NextResponse.next()
// 记录响应时间
Promise.resolve().then(() => {
const duration = Date.now() - startTime
recordMetric(pathname, duration)
// 如果响应时间过长,发送警告
if (duration > 1000) {
console.warn(`Slow response: ${pathname} took ${duration}ms`)
}
})
return response
}错误处理
中间件错误处理
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
try {
// 中间件逻辑
const { pathname } = request.nextUrl
// 可能抛出错误的操作
if (pathname === '/error-test') {
throw new Error('测试错误')
}
return NextResponse.next()
} catch (error) {
console.error('Middleware error:', error)
// 返回错误响应
return new NextResponse('Internal Server Error', {
status: 500,
headers: {
'Content-Type': 'text/plain',
},
})
}
}测试中间件
单元测试
typescript
// __tests__/middleware.test.ts
import { NextRequest } from 'next/server'
import { middleware } from '../middleware'
describe('Middleware', () => {
it('should redirect unauthenticated users', async () => {
const request = new NextRequest('http://localhost:3000/dashboard')
const response = await middleware(request)
expect(response.status).toBe(307) // 重定向状态码
expect(response.headers.get('location')).toContain('/login')
})
it('should allow authenticated users', async () => {
const request = new NextRequest('http://localhost:3000/dashboard', {
headers: {
cookie: 'token=valid-jwt-token'
}
})
const response = await middleware(request)
expect(response.status).toBe(200)
})
})最佳实践
1. 性能优化
typescript
// ✅ 好的做法 - 早期返回
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 跳过静态资源
if (pathname.startsWith('/_next/') || pathname.includes('.')) {
return NextResponse.next()
}
// 其他逻辑...
}
// ❌ 避免 - 复杂的同步操作
export function middleware(request: NextRequest) {
// 避免复杂的计算
const result = heavyComputation()
return NextResponse.next()
}2. 错误处理
typescript
// ✅ 好的做法
export async function middleware(request: NextRequest) {
try {
const user = await verifyToken(token)
return NextResponse.next()
} catch (error) {
console.error('Auth error:', error)
return NextResponse.redirect(new URL('/login', request.url))
}
}3. 匹配器配置
typescript
// ✅ 好的做法 - 精确匹配
export const config = {
matcher: [
'/dashboard/:path*',
'/api/protected/:path*',
'/((?!_next/static|_next/image|favicon.ico).*)',
],
}
// ❌ 避免 - 过于宽泛的匹配
export const config = {
matcher: '/:path*', // 会匹配所有路径
}总结
Next.js 中间件的关键特性:
- 请求拦截 - 在请求到达页面前执行逻辑
- 灵活响应 - 重定向、重写、修改响应
- 性能优化 - 边缘计算,减少服务器负载
- 安全增强 - 认证、授权、CSRF 保护
- 监控分析 - 请求日志、性能指标
合理使用中间件可以提升应用的安全性、性能和用户体验。