Next.js 中间件实战:鉴权、重定向与 A/B 分流

Next.js 中间件实战:鉴权、重定向与 A/B 分流

Next.js 的 middleware.ts 是个特别容易被误用的东西。它跑在每一个请求进入页面之前 ,而且运行在 Edge Runtime------这意味着你不能在里面连数据库、不能用 Node 的 fs,还得小心它会拦截到静态资源。这篇用三个真实场景(登录鉴权、按地区重定向、A/B 分流)把中间件讲透,顺带把新手最常踩的 matcher 配置坑和 Edge Runtime 限制说清楚。

中间件长什么样,它在哪一层跑

在项目根目录(和 app/ 同级)建一个 middleware.ts:

typescript 复制代码
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  console.log('请求路径:', request.nextUrl.pathname)
  return NextResponse.next()   // 放行,继续走到目标页面
}

它跑在请求到达页面/路由之前,在 CDN 边缘节点执行(Edge Runtime),比页面渲染更早。所以它适合做「所有请求都要先过一遍」的横切逻辑:鉴权、重定向、改 header、埋点。

NextResponse.next() 表示放行;你也可以返回 redirectrewrite 或直接一个 Response 来中断。

坑一:matcher 不配好,静态资源全被拦

新手最常见的问题:写完中间件发现图片、CSS、_next 里的 JS 全都走了一遍中间件,页面卡顿甚至白屏。因为默认情况下中间件匹配所有路径 。必须用 config.matcher 精确圈定范围:

typescript 复制代码
export const config = {
  matcher: [
    /*
     * 匹配除以下之外的所有路径:
     * - _next/static (构建产物)
     * - _next/image (图片优化)
     * - favicon.ico、public 下的静态文件
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

这条负向前瞻的正则是官方推荐写法。没有它,中间件会白白在每个静态资源请求上跑一次,既浪费又可能出 bug。记住:matcher 是中间件性能的第一道闸。

场景一:登录鉴权与重定向

最常见的用途:未登录用户访问 /dashboard 时踢回登录页,并带上原始地址方便登录后跳回。

typescript 复制代码
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  const { pathname } = request.nextUrl

  // 受保护路由:没 token 就重定向到登录页
  if (pathname.startsWith('/dashboard')) {
    if (!token) {
      const loginUrl = new URL('/login', request.url)
      // 把用户原本想去的地址塞进 query,登录后可跳回
      loginUrl.searchParams.set('from', pathname)
      return NextResponse.redirect(loginUrl)
    }
  }

  // 已登录还去登录页?直接送回 dashboard
  if (pathname === '/login' && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
}

几个关键点:

  • request.cookies.get() 是中间件里读 cookie 的方式,和页面里的 cookies() 不同。
  • 构造 URL 一定要用 new URL(path, request.url),把当前请求的 origin 带上,否则重定向会丢掉域名。
  • ⚠️ 不要在中间件里验证 JWT 签名的完整业务逻辑或查数据库。Edge Runtime 没有 Node 的 crypto/db 能力,而且中间件要快。这里只做「有没有 token」这种轻量判断;真正的权限校验放到页面或 API 层。中间件是「粗筛」,不是「终审」。

场景二:按地区/语言重定向

利用中间件能读到地理信息和请求头,做多语言或多地区分流:

typescript 复制代码
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  // 已经带语言前缀就放行,避免无限重定向
  if (pathname.startsWith('/zh') || pathname.startsWith('/en')) {
    return NextResponse.next()
  }

  // 从 Accept-Language 头粗略判断语言
  const accept = request.headers.get('accept-language') || ''
  const locale = accept.startsWith('zh') ? 'zh' : 'en'

  // 重写(rewrite)而不是重定向:URL 不变,内部指向带语言前缀的页面
  return NextResponse.rewrite(new URL(`/${locale}${pathname}`, request.url))
}

这里用了 rewrite 而不是 redirect,区别很重要:

  • redirect:浏览器地址栏会变,发生一次 302 跳转,用户能看到 URL 变化。
  • rewrite :地址栏不变,服务端偷偷把请求指向另一个路径。用户以为访问的是 /about,实际渲染的是 /zh/about

多语言、灰度这类「对用户透明」的场景用 rewrite;「地址确实该变」(如未登录踢到登录页)用 redirect

场景三:A/B 分流(灰度实验)

想让 50% 用户看新版首页、50% 看旧版,且同一用户每次访问结果一致。核心是:用 cookie 记住分桶,没分桶的随机分一次并写回。

typescript 复制代码
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  if (pathname !== '/') return NextResponse.next()

  // 读已有分桶
  let bucket = request.cookies.get('ab-home')?.value

  // 没分过桶:随机分配 A/B
  if (bucket !== 'a' && bucket !== 'b') {
    bucket = Math.random() < 0.5 ? 'a' : 'b'
  }

  // B 组重写到新版首页;A 组走默认
  const url = bucket === 'b'
    ? new URL('/home-v2', request.url)
    : request.nextUrl.clone()

  const res = NextResponse.rewrite(url)
  // 把分桶写进 cookie,保证用户后续访问稳定看到同一版本
  res.cookies.set('ab-home', bucket, {
    maxAge: 60 * 60 * 24 * 30,   // 30 天
    path: '/',
  })
  return res
}

export const config = {
  matcher: ['/'],
}

要点:

  • 分桶结果必须落 cookie 。否则每次刷新都重新 Math.random(),同一个用户一会儿 A 一会儿 B,实验数据就废了。
  • 在响应对象上 res.cookies.set() 写回,而不是在 request 上。中间件里 request 的 cookie 是只读视图,写要写在返回的 response 上。
  • A/B 分流放在中间件层的好处:分流发生在渲染之前,用户直接拿到对应版本的页面,不会出现「先闪一下 A 再跳 B」的抖动。

Edge Runtime 的硬限制,别踩

中间件跑在 Edge Runtime,和 Node 环境不是一回事,以下这些用不了:

  • Node 内置模块:fsnetchild_process 等一律不可用。
  • 大部分依赖 Node API 的 npm 包(比如某些完整的数据库驱动)。
  • 长时间运行的逻辑------中间件有执行时间上限,别在里面做重活。

如果发现「本地能跑、部署到 Vercel 报错」,十有八九是在中间件里用了 Edge 不支持的 API。解决思路:把重逻辑挪到 Route Handler(app/api/.../route.ts)或 Server Component,中间件只留最轻量的判断和重定向。

小结

  • 中间件跑在所有页面请求之前、Edge Runtime 上,适合鉴权粗筛、重定向、A/B 分流这类横切逻辑。
  • config.matcher 必须配 ,排除 _next 和静态资源,否则中间件会拖慢每一个资源请求。
  • redirect 改地址栏、rewrite 不改:对用户透明的分流/多语言用 rewrite,该跳转的用 redirect。
  • 鉴权在中间件里只做「有没有 token」的粗筛,真正的权限校验放页面/API 层;别在 Edge Runtime 里查库或做重活。
  • A/B 分流务必把分桶结果写进 cookie,并写在 response 上,保证同一用户体验一致。

一句话记忆点:中间件是请求的「安检口」------它决定谁能进、往哪走,但别把它当成处理业务的「办公室」。

相关推荐
广州灵眸科技有限公司1 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
乐观勇敢坚强的老彭1 小时前
C++浮点数使用注意事项
开发语言·c++
wuyk5552 小时前
38.什么是C语言?从语法到工程落地的工程师视角解读
c语言·开发语言·stm32·单片机·mcu·51单片机
库克克2 小时前
【C++ 】内联函数
java·开发语言·c++
zh_xuan2 小时前
java 虚拟线程
java·开发语言·虚拟线程
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:ModalBottomSheet 实战
android·javascript·学习·flutter·华为·harmonyos
江华森2 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
jason成都2 小时前
RTKLIB Java 移植踩坑复盘:卡尔曼滤波核心逻辑修复与滑坡场景实测验证
java·开发语言
一次旅行2 小时前
【AI工具】Rust-Based CLI:用 xargs 和并行加速你的 Linux 日常
linux·开发语言·rust