Next.js 中间件在请求完成之前运行,允许你通过重写、重定向、修改请求头或直接返回响应来修改响应。它在 Edge Runtime 上执行,对于认证检查、基于地理位置的重定向、A/B 测试和速率限制等任务非常快速。
什么是 Next.js 中间件?
中间件是在请求到达页面或 API 路由之前运行的函数。它运行在 Edge Runtime 上,在最近的 CDN 边缘执行。
// Request flow in Next.js
//
// Client Request
// │
// ▼
// ┌─────────────┐
// │ Middleware │ ← Runs here (Edge Runtime)
// └─────────────┘
// │
// ▼
// ┌─────────────┐
// │ Page/API │ ← Your application code
// └─────────────┘
// │
// ▼
// Client Response基本中间件设置
在项目根目录创建 middleware.ts 文件。
// middleware.ts (project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Log every request
console.log(`[${request.method}] ${request.nextUrl.pathname}`);
// Continue to the next handler
return NextResponse.next();
}
// Only run on specific paths
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};配置匹配器
matcher 配置决定哪些路由触发中间件。
// Match specific routes
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};
// Match all routes except static files and API
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
// Multiple matchers with different patterns
export const config = {
matcher: [
'/dashboard/:path*', // All dashboard routes
'/admin/:path*', // All admin routes
'/api/protected/:path*', // Protected API routes
'/profile', // Exact match
],
};
// Conditional matching with has/missing (Next.js 13.1+)
export const config = {
matcher: [
{
source: '/api/:path*',
has: [
{ type: 'header', key: 'authorization' },
],
},
],
};认证中间件
中间件最常见的用途之一是保护需要认证的路由。
JWT 令牌验证
在中间件中直接验证 JWT 令牌,提供边缘级认证。
// middleware.ts — JWT Authentication
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!
);
const PUBLIC_PATHS = ['/login', '/register', '/forgot-password'];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip public paths
if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) {
return NextResponse.next();
}
// Get token from cookie or Authorization header
const token =
request.cookies.get('auth-token')?.value ||
request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
// Redirect to login with return URL
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}
try {
// Verify JWT token
const { payload } = await jwtVerify(token, JWT_SECRET);
// Add user info to headers for downstream use
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 (error) {
// Token expired or invalid — redirect to login
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
const response = NextResponse.redirect(loginUrl);
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*', '/api/protected/:path*'],
};基于角色的访问控制
中间件可以通过检查用户角色来执行访问控制。
// Role-based access control in middleware
const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/admin': ['admin'],
'/dashboard': ['admin', 'editor', 'viewer'],
'/editor': ['admin', 'editor'],
'/api/admin': ['admin'],
};
function hasPermission(pathname: string, role: string): boolean {
for (const [route, roles] of Object.entries(ROUTE_PERMISSIONS)) {
if (pathname.startsWith(route)) {
return roles.includes(role);
}
}
return true; // No restriction for unmatched routes
}
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
if (!token) return NextResponse.redirect(new URL('/login', request.url));
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
const role = payload.role as string;
const { pathname } = request.nextUrl;
if (!hasPermission(pathname, role)) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}重定向模式
中间件非常适合基于各种条件处理重定向。
基于地理位置的重定向
使用请求中的地理位置数据重定向用户。
// Geo-based redirect (Vercel Edge)
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
const city = request.geo?.city || 'Unknown';
// Redirect EU users to GDPR-compliant page
const EU_COUNTRIES = [
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI',
'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU',
'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
];
if (EU_COUNTRIES.includes(country) &&
request.nextUrl.pathname === '/') {
return NextResponse.redirect(new URL('/eu', request.url));
}
// Add geo headers for downstream use
const response = NextResponse.next();
response.headers.set('x-country', country);
response.headers.set('x-city', city);
return response;
}语言检测和重定向
自动检测用户首选语言并重定向到相应的语言路由。
// Locale detection middleware
const SUPPORTED_LOCALES = ['en', 'fr', 'de', 'es', 'ja', 'ko', 'zh'];
const DEFAULT_LOCALE = 'en';
function getPreferredLocale(request: NextRequest): string {
// Check cookie first (user preference)
const cookieLocale = request.cookies.get('locale')?.value;
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
return cookieLocale;
}
// Parse Accept-Language header
const acceptLang = request.headers.get('accept-language') || '';
const preferred = acceptLang
.split(',')
.map(lang => lang.split(';')[0].trim().substring(0, 2))
.find(lang => SUPPORTED_LOCALES.includes(lang));
return preferred || DEFAULT_LOCALE;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if locale is already in the path
const hasLocale = SUPPORTED_LOCALES.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return NextResponse.next();
// Redirect to locale-prefixed path
const locale = getPreferredLocale(request);
const url = new URL(`/${locale}${pathname}`, request.url);
return NextResponse.redirect(url);
}速率限制
在边缘实现速率限制以保护 API 路由。
// Simple rate limiting with in-memory store
// Note: This works per-edge-node; use Redis for distributed rate limiting
const rateLimit = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT = 100; // requests
const RATE_WINDOW = 60 * 1000; // per minute
function isRateLimited(ip: string): boolean {
const now = Date.now();
const record = rateLimit.get(ip);
if (!record || now > record.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + RATE_WINDOW });
return false;
}
record.count++;
return record.count > RATE_LIMIT;
}
export function middleware(request: NextRequest) {
// Only rate-limit API routes
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ||
request.ip ||
'127.0.0.1';
if (isRateLimited(ip)) {
return new NextResponse(
JSON.stringify({ error: 'Too many requests' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': '60',
},
}
);
}
return NextResponse.next();
}滑动窗口速率限制器
使用滑动窗口算法平滑速率限制。
// Rate limiting with Upstash Redis (production-ready)
// npm install @upstash/ratelimit @upstash/redis
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
analytics: true,
prefix: 'api-ratelimit',
});
export async function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const ip = request.ip ?? '127.0.0.1';
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
const response = success
? NextResponse.next()
: new NextResponse(JSON.stringify({ error: 'Rate limit exceeded' }), {
status: 429,
headers: { 'Content-Type': 'application/json' },
});
// Always set rate limit headers
response.headers.set('X-RateLimit-Limit', limit.toString());
response.headers.set('X-RateLimit-Remaining', remaining.toString());
response.headers.set('X-RateLimit-Reset', reset.toString());
return response;
}响应头和安全性
中间件是设置安全头的理想位置。
安全头
// Security headers middleware
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Prevent XSS attacks
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
// Prevent clickjacking
response.headers.set('X-Frame-Options', 'DENY');
// Prevent MIME type sniffing
response.headers.set('X-Content-Type-Options', 'nosniff');
// Enable HSTS
response.headers.set(
'Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload'
);
// Referrer policy
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions policy
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
return response;
}CORS 头
在中间件级别处理跨域资源共享。
// CORS middleware for API routes
const ALLOWED_ORIGINS = [
'https://myapp.com',
'https://staging.myapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '',
].filter(Boolean);
export function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const origin = request.headers.get('origin') || '';
const isAllowed = ALLOWED_ORIGINS.includes(origin);
// Handle preflight OPTIONS request
if (request.method === 'OPTIONS') {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': isAllowed ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
});
}
const response = NextResponse.next();
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}A/B 测试
中间件支持无闪烁的服务端 A/B 测试。
// A/B Testing middleware
export function middleware(request: NextRequest) {
// Only A/B test the homepage
if (request.nextUrl.pathname !== '/') {
return NextResponse.next();
}
// Check if user already has a variant assigned
const variant = request.cookies.get('ab-variant')?.value;
if (variant) {
// Rewrite to the variant page without changing URL
return NextResponse.rewrite(
new URL(`/variants/${variant}`, request.url)
);
}
// Assign a random variant (50/50 split)
const newVariant = Math.random() < 0.5 ? 'control' : 'experiment';
const response = NextResponse.rewrite(
new URL(`/variants/${newVariant}`, request.url)
);
// Persist variant in cookie for 30 days
response.cookies.set('ab-variant', newVariant, {
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
sameSite: 'lax',
});
return response;
}URL 重写
在不改变浏览器 URL 的情况下重写 URL。
// Multi-tenant rewriting
// app.mysite.com → /tenants/app
// docs.mysite.com → /tenants/docs
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const subdomain = hostname.split('.')[0];
if (subdomain && subdomain !== 'www' && subdomain !== 'mysite') {
return NextResponse.rewrite(
new URL(`/tenants/${subdomain}${request.nextUrl.pathname}`, request.url)
);
}
return NextResponse.next();
}
// Proxy API requests to external service
// /api/external/* → https://api.external-service.com/*
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/external/')) {
const path = request.nextUrl.pathname.replace('/api/external', '');
return NextResponse.rewrite(
new URL(`https://api.external-service.com${path}`)
);
}
return NextResponse.next();
}链式中间件
通过组合多个中间件函数来链接它们。
// Composing multiple middleware functions
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
type MiddlewareFn = (
request: NextRequest,
response: NextResponse
) => NextResponse | Promise<NextResponse>;
function chain(
functions: MiddlewareFn[],
request: NextRequest
): Promise<NextResponse> {
return functions.reduce(
async (promise, fn) => {
const response = await promise;
return fn(request, response);
},
Promise.resolve(NextResponse.next())
);
}
// Individual middleware functions
const withLogging: MiddlewareFn = (request, response) => {
console.log(`[${request.method}] ${request.nextUrl.pathname}`);
return response;
};
const withSecurityHeaders: MiddlewareFn = (request, response) => {
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
return response;
};
const withAuth: MiddlewareFn = (request, response) => {
const token = request.cookies.get('auth-token')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return response;
};
// Compose all middleware
export function middleware(request: NextRequest) {
return chain([withLogging, withSecurityHeaders, withAuth], request);
}Edge Runtime 限制
中间件运行在 Edge Runtime 上,有一些限制。
- 无文件系统访问
- 无原生 Node.js 模块
- 仅限 Web API
- 包大小限制 1MB
- 无数据库连接
- 执行时间限制
// What you CAN use in Edge Runtime:
// ✅ fetch, Request, Response, Headers, URL
// ✅ TextEncoder, TextDecoder
// ✅ crypto.subtle (Web Crypto API)
// ✅ structuredClone
// ✅ atob, btoa
// ✅ URLPattern
// ✅ EdgeDB, Upstash, PlanetScale drivers
// What you CANNOT use:
// ❌ fs (file system)
// ❌ path, os, child_process
// ❌ pg, mysql2, mongoose (traditional DB drivers)
// ❌ bcrypt (use bcryptjs or Web Crypto instead)
// ❌ Large npm packages that exceed 1MB bundle调试中间件
使用这些技术来检查和排查中间件问题。
// Debugging middleware
export function middleware(request: NextRequest) {
// Log request details
console.log({
method: request.method,
url: request.url,
pathname: request.nextUrl.pathname,
cookies: Object.fromEntries(request.cookies.getAll().map(c => [c.name, c.value])),
headers: Object.fromEntries(request.headers),
geo: request.geo,
ip: request.ip,
});
// Return debug info in response headers (dev only)
const response = NextResponse.next();
if (process.env.NODE_ENV === 'development') {
response.headers.set('x-middleware-pathname', request.nextUrl.pathname);
response.headers.set('x-middleware-matched', 'true');
}
return response;
}最佳实践
- 保持中间件轻量。
- 使用 matcher 排除静态文件。
- 避免在中间件中调用数据库。
- 设置适当的缓存头。
- 使用 cookie 持久化 A/B 测试。
- 实现错误处理。
- 测试不同请求场景。
- 使用环境变量配置。
总结
Next.js 中间件是在边缘运行的强大工具,可实现快速认证检查、智能重定向、速率限制、A/B 测试和安全头管理。保持中间件精简,使用 matcher 定向特定路由,充分利用 Edge Runtime 能力以获得最佳性能。
常见问题
Next.js 中间件会在每个请求上运行吗?
默认是的,但应使用 matcher 限制到特定路由。
可以在中间件中使用数据库吗?
不能直接使用。使用 fetch 调用 API 路由或使用边缘兼容数据库。
中间件的 redirect 和 rewrite 有什么区别?
redirect 改变浏览器 URL,rewrite 不改变浏览器 URL 但提供不同内容。
如何处理中间件中的错误?
使用 try-catch 包装逻辑,错误时返回适当响应。