Next.js
代理
深入理解 Next.js Proxy 功能,包括请求拦截、重写、重定向、Headers 和 Cookies 处理
概述
proxy.js|ts 文件用于编写 Proxy 并在请求完成之前在服务器上运行代码。然后,根据传入的请求,你可以通过重写、重定向、修改请求或响应 Headers,或直接响应来修改响应。
Proxy 在路由渲染之前执行。它特别适用于实现自定义服务器端逻辑,如身份验证、日志记录或处理重定向。
Proxy 旨在与渲染代码分开调用,在优化情况下部署到 CDN 以实现快速重定向/重写处理,你不应尝试依赖共享模块或全局变量。
要将信息从 Proxy 传递到应用程序,请使用 Headers、Cookies、重写、重定向或 URL。
约定
在项目根目录或 src 目录内创建 proxy.ts(或 .js)文件,使其与 pages 或 app 位于同一级别。
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}基本用法
简单重定向
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}条件逻辑
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
if (request.nextUrl.pathname.startsWith('/blog')) {
return NextResponse.redirect(new URL('/news', request.url))
}
}Matcher 配置
使用 matcher 配置 Proxy 在特定路径上运行。
单个路径
export const config = {
matcher: '/about/:path*',
}多个路径
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}使用正则表达式
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).*)',
],
}排除特定路径
export const config = {
matcher: [
/*
* 匹配所有路径,除了:
* - /api
* - /_next
* - /static
*/
'/((?!api|_next|static).*)',
],
}NextRequest
NextRequest 扩展了 Web Request API,提供了额外的便捷方法。
访问 URL
import { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const url = request.nextUrl
const pathname = url.pathname
const searchParams = url.searchParams
console.log('Pathname:', pathname)
console.log('Search params:', searchParams.toString())
}访问 Cookies
import { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}访问 Headers
import { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const userAgent = request.headers.get('user-agent')
const authorization = request.headers.get('authorization')
console.log('User Agent:', userAgent)
console.log('Authorization:', authorization)
}访问 IP 地址
import { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const ip = request.ip || request.headers.get('x-forwarded-for')
console.log('IP Address:', ip)
}访问地理位置
import { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const geo = request.geo
console.log('Country:', geo?.country)
console.log('Region:', geo?.region)
console.log('City:', geo?.city)
}NextResponse
NextResponse 扩展了 Web Response API,提供了额外的便捷方法。
重定向
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}永久重定向
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url), 301)
}重写
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}设置 Headers
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'custom-value')
response.headers.set('x-version', '1.0')
return response
}设置 Cookies
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
response.cookies.set('token', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 1 week
})
return response
}删除 Cookies
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
response.cookies.delete('token')
return response
}直接响应
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
return new NextResponse('Unauthorized', { status: 401 })
}条件逻辑
基于路径的条件
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/admin')) {
const token = request.cookies.get('admin-token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
if (pathname.startsWith('/api')) {
const apiKey = request.headers.get('x-api-key')
if (!apiKey) {
return new NextResponse('API key required', { status: 401 })
}
}
return NextResponse.next()
}基于查询参数的条件
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { searchParams } = request.nextUrl
const preview = searchParams.get('preview')
if (preview === 'true') {
request.nextUrl.pathname = `/preview${request.nextUrl.pathname}`
return NextResponse.rewrite(request.nextUrl)
}
return NextResponse.next()
}基于 Headers 的条件
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const userAgent = request.headers.get('user-agent') || ''
if (userAgent.includes('Mobile')) {
return NextResponse.rewrite(new URL('/mobile', request.url))
}
return NextResponse.next()
}基于地理位置的条件
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const country = request.geo?.country
if (country === 'CN') {
return NextResponse.rewrite(new URL('/cn', request.url))
}
if (country === 'US') {
return NextResponse.rewrite(new URL('/us', request.url))
}
return NextResponse.next()
}身份验证
基本身份验证
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// 保护 /dashboard 路由
if (pathname.startsWith('/dashboard')) {
const token = request.cookies.get('session-token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 验证 token
const isValid = verifyToken(token.value)
if (!isValid) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
function verifyToken(token: string): boolean {
// 实现 token 验证逻辑
return true
}JWT 验证
import { NextResponse, NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/api/protected')) {
const token = request.headers.get('authorization')?.split(' ')[1]
if (!token) {
return new NextResponse('Unauthorized', { status: 401 })
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
await jwtVerify(token, secret)
} catch (error) {
return new NextResponse('Invalid token', { status: 401 })
}
}
return NextResponse.next()
}角色基础访问控制
import { NextResponse, NextRequest } from 'next/server'
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/admin')) {
const sessionToken = request.cookies.get('session-token')
if (!sessionToken) {
return NextResponse.redirect(new URL('/login', request.url))
}
const user = await getUserFromToken(sessionToken.value)
if (user.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
}
return NextResponse.next()
}
async function getUserFromToken(token: string) {
// 实现从 token 获取用户的逻辑
return { role: 'admin' }
}国际化
基于地理位置的语言重定向
import { NextResponse, NextRequest } from 'next/server'
const locales = ['en', 'zh', 'ja', 'ko']
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// 检查路径是否已包含语言前缀
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) {
return NextResponse.next()
}
// 根据地理位置确定语言
const country = request.geo?.country
let locale = 'en'
if (country === 'CN') locale = 'zh'
if (country === 'JP') locale = 'ja'
if (country === 'KR') locale = 'ko'
// 重定向到带语言前缀的路径
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}基于 Cookie 的语言选择
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
const locale = request.cookies.get('locale')?.value || 'en'
if (!pathname.startsWith(`/${locale}`)) {
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
return NextResponse.next()
}A/B 测试
基于 Cookie 的 A/B 测试
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname === '/') {
let bucket = request.cookies.get('bucket')?.value
if (!bucket) {
// 随机分配到 A 或 B
bucket = Math.random() < 0.5 ? 'a' : 'b'
const response = NextResponse.next()
response.cookies.set('bucket', bucket)
return response
}
if (bucket === 'b') {
return NextResponse.rewrite(new URL('/variant-b', request.url))
}
}
return NextResponse.next()
}速率限制
基于 IP 的速率限制
import { NextResponse, NextRequest } from 'next/server'
const rateLimit = new Map<string, number[]>()
export function proxy(request: NextRequest) {
const ip = request.ip || 'unknown'
const now = Date.now()
const windowMs = 60 * 1000 // 1 minute
const maxRequests = 10
const requests = rateLimit.get(ip) || []
const recentRequests = requests.filter((time) => now - time < windowMs)
if (recentRequests.length >= maxRequests) {
return new NextResponse('Too many requests', { status: 429 })
}
recentRequests.push(now)
rateLimit.set(ip, recentRequests)
return NextResponse.next()
}日志记录
请求日志
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl
const method = request.method
const userAgent = request.headers.get('user-agent')
console.log({
timestamp: new Date().toISOString(),
method,
pathname,
search,
userAgent,
})
return NextResponse.next()
}功能标志
基于环境的功能标志
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/new-feature')) {
const featureEnabled = process.env.NEW_FEATURE_ENABLED === 'true'
if (!featureEnabled) {
return NextResponse.redirect(new URL('/coming-soon', request.url))
}
}
return NextResponse.next()
}基于用户的功能标志
import { NextResponse, NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/beta')) {
const userId = request.cookies.get('user-id')?.value
if (!userId || !isBetaUser(userId)) {
return NextResponse.redirect(new URL('/not-available', request.url))
}
}
return NextResponse.next()
}
function isBetaUser(userId: string): boolean {
// 检查用户是否是 beta 测试用户
return true
}执行顺序
Proxy 在以下顺序中执行:
headersfromnext.config.jsredirectsfromnext.config.js- Proxy (
proxy.js) beforeFiles(rewrites) fromnext.config.js- Filesystem routes (
public/,_next/static/,pages/,app/, etc.) afterFiles(rewrites) fromnext.config.js- Dynamic Routes (
/blog/[slug]) fallback(rewrites) fromnext.config.js
运行时
Proxy 支持 Edge 和 Node.js 运行时。
Edge 运行时(默认)
export const config = {
runtime: 'edge',
}Node.js 运行时
export const config = {
runtime: 'nodejs',
}从 Middleware 迁移
middleware 文件约定已弃用并重命名为 proxy。
运行 Codemod
npx @next/codemod@canary middleware-to-proxy .手动迁移
- // middleware.ts
+ // proxy.ts
- export function middleware() {
+ export function proxy() {
return NextResponse.next()
}最佳实践
1. 保持 Proxy 轻量
// 好的做法 - 快速检查和重定向
export function proxy(request: NextRequest) {
if (!request.cookies.has('token')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
// 不好的做法 - 复杂的数据库查询
export async function proxy(request: NextRequest) {
const user = await db.user.findUnique({ where: { id: userId } })
// 避免在 Proxy 中进行复杂操作
}2. 使用精确的 Matcher
// 好的做法 - 精确匹配
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
// 不好的做法 - 过于宽泛
export const config = {
matcher: '/:path*',
}3. 避免共享状态
// 不好的做法 - 使用全局变量
let requestCount = 0
export function proxy(request: NextRequest) {
requestCount++ // 不要这样做
return NextResponse.next()
}4. 使用环境变量
export function proxy(request: NextRequest) {
const apiKey = process.env.API_KEY
if (!apiKey) {
console.error('API key not configured')
}
return NextResponse.next()
}5. 处理错误
export async function proxy(request: NextRequest) {
try {
const token = request.cookies.get('token')?.value
if (token) {
await verifyToken(token)
}
} catch (error) {
console.error('Token verification failed:', error)
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}总结
Next.js Proxy 提供了强大的请求拦截和处理功能:
- 请求拦截:在路由渲染之前拦截请求
- 重定向和重写:灵活的路由控制
- Headers 和 Cookies:修改请求和响应
- 条件逻辑:基于路径、查询参数、Headers 等的条件处理
- 身份验证:实现身份验证和授权
- 国际化:基于地理位置的语言重定向
- A/B 测试:实现 A/B 测试
- 速率限制:防止滥用
- 功能标志:控制功能可用性
- 运行时选择:支持 Edge 和 Node.js 运行时
通过合理使用 Proxy,可以实现强大的服务器端逻辑,提升应用的性能和安全性。
在 GitHub 上编辑
上次更新于