中间件
中间件(Middleware)允许在请求完成之前运行代码。可以用于认证、日志、重写、重定向等场景。
基本用法
在项目根目录创建 middleware.ts 文件:
// 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();
}
中间件执行顺序
- 中间件
- 静态文件(如果匹配)
- 路由处理器
- 页面组件
路由匹配
匹配器配置
使用 matcher 配置指定中间件应用的路径:
export const config = {
matcher: "/about",
};
多路径匹配
export const config = {
matcher: ["/about", "/contact"],
};
路径参数
export const config = {
matcher: ["/blog/:path*"],
};
排除路径
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
常见用例
认证保护
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (!token) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*"],
};
日志记录
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const start = Date.now();
const response = NextResponse.next();
const duration = Date.now() - start;
console.log(`${request.method} ${request.nextUrl.pathname} - ${duration}ms`);
return response;
}
地理重定向
Next.js 15 移除了 request.geo 和 request.ip 属性,这些值现在由托管服务提供商提供。如果使用 Vercel,可以通过 @vercel/functions 包获取:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { geolocation } from "@vercel/functions";
export function middleware(request: NextRequest) {
const { country } = geolocation(request);
if (country === "CN") {
return NextResponse.redirect(new URL("/cn", request.url));
}
return NextResponse.next();
}
如果需要在本地开发时获取 IP 地址:
import { ipAddress } from "@vercel/functions";
export function middleware(request: NextRequest) {
const ip = ipAddress(request);
console.log("客户端 IP:", ip);
return NextResponse.next();
}
注意
@vercel/functions 包仅在 Vercel 部署时有效。对于其他托管平台,请参考对应平台的文档获取地理位置和 IP 信息。
请求重写
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/old")) {
const newUrl = new URL(
request.nextUrl.pathname.replace("/api/old", "/api/new"),
request.url
);
return NextResponse.rewrite(newUrl);
}
return NextResponse.next();
}
设置请求头
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set("x-custom-header", "custom-value");
response.headers.set("x-pathname", request.nextUrl.pathname);
return response;
}
CORS 处理
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.method === "OPTIONS") {
return new NextResponse(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
const response = NextResponse.next();
response.headers.set("Access-Control-Allow-Origin", "*");
response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
return response;
}
export const config = {
matcher: "/api/:path*",
};
响应操作
重定向
return NextResponse.redirect(new URL("/login", request.url));
重写
return NextResponse.rewrite(new URL("/new-path", request.url));
JSON 响应
return NextResponse.json(
{ error: "未授权" },
{ status: 401 }
);
设置 Cookie
const response = NextResponse.next();
response.cookies.set("token", "jwt-token", {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 60 * 60 * 24 * 7,
});
return response;
删除 Cookie
const response = NextResponse.next();
response.cookies.delete("token");
return response;
高级用法
基于角色的访问控制
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const roleRoutes: Record<string, string[]> = {
admin: ["/admin", "/dashboard"],
editor: ["/editor", "/dashboard"],
user: ["/dashboard"],
};
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
const role = request.cookies.get("role")?.value || "user";
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
const allowedRoutes = roleRoutes[role] || [];
const pathname = request.nextUrl.pathname;
const isAllowed = allowedRoutes.some((route) =>
pathname.startsWith(route)
);
if (!isAllowed) {
return NextResponse.redirect(new URL("/unauthorized", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/admin/:path*", "/editor/:path*", "/dashboard/:path*"],
};
限流
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const rateLimit = new Map<string, { count: number; lastRequest: number }>();
export function middleware(request: NextRequest) {
// 从请求头获取客户端标识
const forwardedFor = request.headers.get("x-forwarded-for");
const ip = forwardedFor ? forwardedFor.split(",")[0].trim() : "unknown";
const now = Date.now();
const windowMs = 60 * 1000; // 1 分钟
const maxRequests = 100;
const userLimit = rateLimit.get(ip);
if (!userLimit || now - userLimit.lastRequest > windowMs) {
rateLimit.set(ip, { count: 1, lastRequest: now });
} else if (userLimit.count >= maxRequests) {
return NextResponse.json(
{ error: "请求过于频繁" },
{ status: 429 }
);
} else {
rateLimit.set(ip, {
count: userLimit.count + 1,
lastRequest: userLimit.lastRequest,
});
}
return NextResponse.next();
}
注意
这个示例使用内存存储限流数据,仅适用于单实例部署。在生产环境中,建议使用 Redis 等分布式存储来实现限流功能。
小结
本章我们学习了:
- 中间件的基本用法
- 路由匹配配置
- 常见用例(认证、日志、重定向等)
- 响应操作
- 高级用法
练习
- 实现一个认证中间件,保护需要登录的页面
- 创建一个日志中间件,记录所有请求
- 实现基于角色的访问控制
- 创建一个简单的限流中间件