首页 / Next.js 16 入门教程 / Proxy(原 Middleware)

Next.js 16 入门教程

Proxy(原 Middleware)

本教程共 42 篇 · 第 22 篇 · 更新于 2026-07-30 · 约 7 分钟阅读

Next.jsNext.js 16 入门教程ProxyMiddleware中间件认证

22. Proxy(原 Middleware)

本节目标:理解 Next.js 16 Proxy 的工作机制,学会使用 matcher 控制执行范围,掌握请求改写、重定向和认证拦截的常见模式。

Proxy 是什么

Next.js 16 的重要变更

Next.js 16 将 middleware 文件约定重命名为 proxy。原因是 “middleware” 一词容易与 Express.js 中间件混淆,导致误用。proxy 更准确地描述了它的行为——在应用之前代理请求,可以运行在应用主运行时之外,在请求到达应用之前进行处理。

如果你有旧的 middleware.ts 文件,可以运行以下 codemod 自动迁移:

npx @next/codemod@latest middleware-to-proxy .

Proxy(代理)是在请求到达路由处理之前执行的代码。它可以拦截、修改请求和响应,实现认证检查、URL 重写、A/B 测试等功能。

Proxy 默认运行在 Node.js Runtime 上,可以使用完整的 Node.js API。请注意:在 Next.js 16 中,Proxy 文件中不再支持设置 export const runtime = 'edge',否则会抛出错误。

创建 Proxy

在项目根目录(与 app 目录同级)创建 proxy.ts 文件:

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // 你的代理逻辑
  return NextResponse.next()
}

// 配置执行范围
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}

与旧版 Middleware 的区别

Next.js 15 中 Middleware 默认使用 Node.js Runtime,但仍可通过 export const runtime = 'edge' 切换到 Edge Runtime。Next.js 16 的 Proxy 同样默认使用 Node.js Runtime,但移除了 runtime 配置项,设置该选项会抛出错误。

NextResponse 的常用方法

Proxy 通过 NextResponse 对象控制请求流程:

NextResponse.next()

继续正常处理请求,可选择性修改请求头:

export function proxy(request: NextRequest) {
  // 添加自定义请求头,传递给后续处理
  const response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })
  return response
}

NextResponse.redirect()

将用户重定向到另一个 URL:

export function proxy(request: NextRequest) {
  const isLoggedIn = request.cookies.get('session')

  if (!isLoggedIn) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

NextResponse.rewrite()

重写请求路径,浏览器 URL 不变:

export function proxy(request: NextRequest) {
  const locale = request.cookies.get('locale')?.value || 'zh'

  // 将 /products 重写为 /zh/products 或 /en/products
  return NextResponse.rewrite(
    new URL(`/${locale}${request.nextUrl.pathname}`, request.url)
  )
}

直接返回 Response

可以构造自定义响应直接返回,阻止请求继续:

export function proxy(request: NextRequest) {
  // 限流检查
  if (isRateLimited(request)) {
    return new Response('请求过于频繁,请稍后再试', {
      status: 429,
      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
    })
  }

  return NextResponse.next()
}

matcher 配置

matcher 控制 Proxy 在哪些路径上执行。合理配置 matcher 可以避免不必要的性能开销:

export const config = {
  // 匹配多个路径模式
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}

matcher 语法

模式说明示例
/path精确匹配/about
/path/:param单级动态段/blog/:slug
/path/:path*多级动态段(含零级)/docs/:path*
/path/:param?可选参数/blog/:slug?

排除特定路径

使用负向匹配排除不需要 Proxy 的路径:

export const config = {
  // 匹配所有路径,但排除 api、静态资源和图片
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

认证拦截

最常见的 Proxy 用途是认证检查:

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const protectedRoutes = ['/dashboard', '/settings', '/profile']
const publicRoutes = ['/login', '/register', '/']

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  // 检查是否是受保护路由
  const isProtected = protectedRoutes.some((route) =>
    pathname.startsWith(route)
  )

  // 检查用户是否已登录
  const session = request.cookies.get('session')?.value
  const isAuthenticated = Boolean(session)

  // 未登录访问受保护路由 → 重定向到登录页
  if (isProtected && !isAuthenticated) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(loginUrl)
  }

  // 已登录访问登录页 → 重定向到仪表盘
  if (isAuthenticated && pathname === '/login') {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Proxy 不是唯一的安全防线

Proxy 适合做初步的认证检查(如重定向),但真正的权限验证应该在数据访问层(Server Components、Server Actions、Route Handlers)中执行。

国际化路由

使用 Proxy 根据用户偏好自动切换语言:

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const supportedLocales = ['zh', 'en', 'ja']
const defaultLocale = 'zh'

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  // 检查路径是否已包含语言前缀
  const hasLocale = supportedLocales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (hasLocale) return NextResponse.next()

  // 从 cookie 或 Accept-Language 头获取用户偏好
  const locale =
    request.cookies.get('locale')?.value ||
    request.headers.get('accept-language')?.split(',')[0].split('-')[0] ||
    defaultLocale

  const finalLocale = supportedLocales.includes(locale) ? locale : defaultLocale

  return NextResponse.rewrite(
    new URL(`/${finalLocale}${pathname}`, request.url)
  )
}

A/B 测试

使用 Proxy 分配用户到不同的测试组:

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  // 只在首页进行 A/B 测试
  if (pathname !== '/') return NextResponse.next()

  // 检查用户是否已有分组
  let bucket = request.cookies.get('ab-bucket')?.value

  if (!bucket) {
    // 随机分配
    bucket = Math.random() < 0.5 ? 'a' : 'b'
  }

  // 重写到对应版本
  const url = request.nextUrl.clone()
  url.pathname = `/${bucket}${pathname}`
  const response = NextResponse.rewrite(url)

  // 保存分组信息
  if (!request.cookies.get('ab-bucket')) {
    response.cookies.set('ab-bucket', bucket, { maxAge: 60 * 60 * 24 * 30 })
  }

  return response
}

地理位置服务

request.geo 已移除

Next.js 15 起,NextRequest 上的 geoip 属性已被移除。如需获取地理位置信息,请通过请求头读取。不同平台提供不同的头信息:

  • Vercelx-vercel-ip-countryx-vercel-ip-city
  • Cloudflarecf-ipcountry
  • 自托管:需配合反向代理(如 Nginx)注入地理位置头

通过请求头获取地理位置信息:

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // 从请求头获取用户国家信息(不同平台头名不同)
  const country = request.headers.get('x-vercel-ip-country') || 'CN'
  const city = request.headers.get('x-vercel-ip-city') || 'Unknown'

  // 根据地区重写内容
  const url = request.nextUrl.clone()
  url.searchParams.set('country', country)

  return NextResponse.rewrite(url)
}

性能注意事项

Proxy 在每次匹配的请求上都会执行,因此需要注意:

  1. 使用 matcher 缩小范围:避免在所有请求上运行
  2. 保持逻辑轻量:避免复杂计算或数据库查询
  3. 避免阻塞操作:Proxy 中的异步操作会延迟响应
  4. 注意 Cookie 读取cookies() 在 Proxy 中同步读取,但在页面中会触发动态渲染

小结

  1. 创建位置:项目根目录的 proxy.ts(Next.js 16 从 middleware.ts 重命名而来)
  2. 核心方法NextResponse.next()redirect()rewrite()
  3. matcher:精确控制 Proxy 的执行范围
  4. 常见用途:认证拦截、国际化、A/B 测试
  5. 安全原则:Proxy 做初步检查,真正的权限验证在数据层
  6. Next.js 16:Proxy 默认使用 Node.js Runtime,不支持切换到 Edge Runtime;request.georequest.ip 已移除