前置知识

什么是中间件?新手友好指南

从第一原则理解中间件、SaaS 为什么需要它,以及我们的 Next.js 中间件如何处理 i18n 与 request ID — 同时教你如何安全定制。

一句话理解中间件

中间件是在请求进入你的最终处理函数之前运行的代码,用来检查、重定向或添加响应头。


Middleware 101(新手视角)

把一次请求想象成一辆车,行驶到检查站时会被拦下审视。中间件就是这个检查站:所有请求都会经过它。你可以读取请求、做决策,并在需要时修改响应(甚至直接终止)。

常见用途包括:

  • 认证门禁:判断用户是否登录,否则重定向。
  • 国际化(i18n):根据 URL 或偏好路由到正确语言。
  • 日志与链路追踪:附加 request_id 以便全链路关联日志。
  • Feature flag / A/B 测试:尽早分配实验组。
  • 安全与限流:阻止异常流量或强制安全头。

在 Next.js 中,中间件写在 middleware.ts 里,在 edge 上(非常早、非常快)执行。它可以重写/重定向请求、读写头部,并在路由处理前短路响应。


为什么有用(以及什么时候需要)

中间件让“横切关注点”集中处理,也就是很多路由都需要的逻辑:

  • 规则集中:不容易漏掉 auth 或 locale 检查。
  • 可观测性一致:每个请求都有相同的 request_id 和头部。
  • 早期决策:提前重定向或拒绝,减少服务器工作量。
  • Handler 更干净:业务逻辑留在页面/API 里。

SaaS 中典型适用场景:

  • 需要稳定 /:locale/... URL 的多语言应用。
  • 依赖请求级关联 ID 的日志和追踪系统。
  • 付费/管理区域的访问控制(要谨慎处理 Cookie)。
  • A/B 实验、地理路由、头部规范化。

我们的中间件:现在做了什么

我们在 src/middleware.ts 中提供了一个安全、精简的默认实现,将 next-intl 的 i18n 路由与轻量的 request ID 传播组合在一起。

关键行为:

  • 语言路由:通过 next-intl 调用我们的配置(src/i18n/routing.tssrc/i18n/locale.ts),保持 /en/.../fr/... 等 URL 一致,并可选启用自动语言检测。
  • Request ID:为所有响应添加 x-request-id。若请求已有则复用,否则生成 UUID。
  • 保守 matcher:仅在本地化页面和通用路由执行;默认跳过静态资源、Next 内部路由、API 路由和 admin

当前代码(精简版):

// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
import { routing } from '@/i18n/routing';

const intlMiddleware = createMiddleware(routing);

export default function middleware(request: NextRequest) {
  const existing = request.headers.get('x-request-id');
  const requestId = existing || crypto.randomUUID();

  const res = intlMiddleware(request);
  // 仅用于可观测性;不要修改请求的 cookies/headers。
  res.headers.set('x-request-id', requestId);
  return res;
}

export const config = {
  matcher: [
    '/',
    '/(en|en-US|zh|zh-CN|zh-TW|zh-HK|zh-MO|ja|ko|ru|fr|de|ar|es|it)/:path*',
    '/((?!api|_next|_vercel|admin|.*\\..*).*)',
  ],
};

语言路由配置:

// src/i18n/routing.ts
import { defaultLocale, localeDetection, localePrefix, locales } from './locale';
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({ locales, defaultLocale, localePrefix, localeDetection });

语言列表声明(添加/删除从这里改):

// src/i18n/locale.ts
export const locales = ['en', 'zh', 'es', 'fr', 'ja'];
export const defaultLocale = 'en';
export const localePrefix = 'always';
export const localeDetection = process.env.NEXT_PUBLIC_LOCALE_DETECTION === 'true';

注意事项:

  • 为了保持 Better Auth 稳定,我们避免在中间件里修改请求 cookies/headers,只设置响应头。
  • matcher 默认排除 api_next、静态资源和 admin。如果需要可调整。

如何安全定制

先确认这段逻辑是否真的适合中间件(横切逻辑、需要早期判断),而不是写在具体路由中。确认后可参考以下模式:

添加自定义头部

export default function middleware(request: NextRequest) {
  const res = intlMiddleware(request);
  res.headers.set('x-feature-flag', 'on');
  return res;
}

重定向或重写路径

import { NextResponse } from 'next/server';

export default function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  if (pathname === '/old') {
    return NextResponse.redirect(new URL('/new', request.url));
  }
  return intlMiddleware(request);
}

通过 matcher 包含/排除路由

export const config = {
  matcher: [
    '/',
    // 需要对 admin 启用时,移除 negative lookahead
    // 或显式匹配:
    '/admin/:path*',
    '/(en|fr|ja|zh|es)/:path*',
    '/((?!api|_next|_vercel|.*\\..*).*)',
  ],
};

在 handler 中使用 request ID

// 在 Node 的路由处理函数中
import { headers } from 'next/headers';

export async function GET() {
  const h = headers();
  const request_id = h.get('x-request-id');
  // 用于日志、数据库追踪等
  return new Response('ok');
}

国际化调整

  • NEXT_PUBLIC_LOCALE_DETECTION 设为 true 启用自动检测。
  • src/i18n/locale.ts 中添加/删除语言。
  • 新增语言时保持 content/docsmessages/src/i18n 同步。

认证与敏感流程

  • 若在中间件做认证门禁,避免修改 Cookie,尽量使用只读检查和重定向。
  • 保护 API 路由时,handler 层也要做校验 — 中间件只是辅助。

什么时候不该用中间件

  • 只适用于某条路由的逻辑。
  • 重量级异步操作(如数据库调用)。
  • 大请求体转换(中间件无法读取 body)。

保持中间件简洁、快速、聚焦。如果它变复杂,把逻辑移到服务函数里,再在 handler 中调用。


TL;DR

  • 中间件在早期、集中执行 — 适合 i18n、头部、重定向和轻量保护。
  • 默认中间件负责语言路由,并设置 x-request-id 便于追踪。
  • 通过 config.matcher、响应头、安保重定向定制;不要操作 auth Cookies。
  • 保持小而快,并在各语言版本保持一致。