Next.js

代理

深入理解 Next.js Proxy 功能,包括请求拦截、重写、重定向、Headers 和 Cookies 处理

概述

proxy.js|ts 文件用于编写 Proxy 并在请求完成之前在服务器上运行代码。然后,根据传入的请求,你可以通过重写、重定向、修改请求或响应 Headers,或直接响应来修改响应。

Proxy 在路由渲染之前执行。它特别适用于实现自定义服务器端逻辑,如身份验证、日志记录或处理重定向。

Proxy 旨在与渲染代码分开调用,在优化情况下部署到 CDN 以实现快速重定向/重写处理,你不应尝试依赖共享模块或全局变量。

要将信息从 Proxy 传递到应用程序,请使用 Headers、Cookies、重写、重定向或 URL。

约定

在项目根目录或 src 目录内创建 proxy.ts(或 .js)文件,使其与 pagesapp 位于同一级别。

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)
}
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 测试

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 在以下顺序中执行:

  1. headers from next.config.js
  2. redirects from next.config.js
  3. Proxy (proxy.js)
  4. beforeFiles (rewrites) from next.config.js
  5. Filesystem routes (public/, _next/static/, pages/, app/, etc.)
  6. afterFiles (rewrites) from next.config.js
  7. Dynamic Routes (/blog/[slug])
  8. fallback (rewrites) from next.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 提供了强大的请求拦截和处理功能:

  1. 请求拦截:在路由渲染之前拦截请求
  2. 重定向和重写:灵活的路由控制
  3. Headers 和 Cookies:修改请求和响应
  4. 条件逻辑:基于路径、查询参数、Headers 等的条件处理
  5. 身份验证:实现身份验证和授权
  6. 国际化:基于地理位置的语言重定向
  7. A/B 测试:实现 A/B 测试
  8. 速率限制:防止滥用
  9. 功能标志:控制功能可用性
  10. 运行时选择:支持 Edge 和 Node.js 运行时

通过合理使用 Proxy,可以实现强大的服务器端逻辑,提升应用的性能和安全性。

在 GitHub 上编辑

上次更新于