You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13中如何链式调用next-auth与多语言中间件?

解决Next.js 13.4.16多中间件链式调用(withLocale + next-auth)问题

核心问题分析

  1. matcher配置冲突:withLocale的匹配规则覆盖大部分非静态资源路径,而middleware.ts仅针对带语言前缀的特定路由,两者范围不一致导致部分路径未被正确处理。
  2. withAuth调用方式错误:withAuth是高阶函数,返回的是中间件函数而非直接处理请求的结果,直接await withAuth(...)无法正确执行鉴权逻辑。

解决方案步骤

1. 统一全局matcher配置

将两个中间件的匹配规则合并到middleware.ts的config中,确保所有需要处理的路径都被覆盖:

  • 保留withLocale需要处理的排除规则(排除静态资源、API等)
  • 加入需要鉴权的带语言前缀的路由规则

2. 正确实现中间件链式调用

先执行withLocale处理多语言逻辑,若返回重定向响应(3xx状态码)则直接返回;若为NextResponse.next(),则将请求传递给withAuth生成的中间件执行鉴权。

修改后的完整代码

middleware.ts

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { withAuth } from "next-auth/middleware"
import withLocale from "@/app/middlewares/withLocale";

// 合并matcher规则:覆盖withLocale需要处理的路径 + 需要鉴权的路由
export const config = {
  matcher: [
    // 匹配withLocale的处理范围(排除静态资源、API等)
    '/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js|manifest.json).*)',
    // 叠加需要鉴权的路由
    '/(en|es|ru)/(chat|intake-form|home|settings|stories|words|admin|me)'
  ],
};

// 预先生成withAuth的中间件实例
const authMiddleware = withAuth({
  callbacks: {
    authorized({ req, token }) {
      // `/admin`路由需要admin角色
      if (req.nextUrl.pathname.includes("/admin")) {
        return token?.userRole === "admin";
      }
      // 指定路由仅需用户已登录
      if (req.nextUrl.pathname.includes("/me") || 
          ['/chat', '/intake-form', '/home', '/settings', '/stories', '/words'].some(path => req.nextUrl.pathname.includes(path))) {
        return !!token;
      }
      // 其他路径默认放行
      return true;
    },
  },
})

export default async function middleware(req: NextRequest) {
  // 第一步:执行多语言中间件
  const localeResponse = withLocale(req);
  
  // 若多语言中间件返回重定向/非next响应,直接返回
  if (localeResponse.status !== 200) {
    return localeResponse;
  }

  // 第二步:执行鉴权中间件
  return authMiddleware(req);
}

withLocale.ts(移除独立matcher)

删除withLocale.ts中的export const config,避免与全局matcher冲突:

import { NextResponse, NextRequest } from 'next/server';
import acceptLanguage from 'accept-language';
import { fallbackLng, languages } from '@/app/i18n/settings';
import { PATH_LOGIN } from '@/config/path';

acceptLanguage.languages(languages);

const cookieName = 'i18next';

export function withLocale(req: NextRequest) {
  if (
    req.nextUrl.pathname.indexOf('icon') > -1 ||
    req.nextUrl.pathname.indexOf('chrome') > -1
  )
    return NextResponse.next();
  let lng: string | null = '';
  if (req.cookies.has(cookieName))
    lng = acceptLanguage.get(req.cookies.get(cookieName)?.value);
  if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'));
  if (!lng) lng = fallbackLng;

  // 排除特定路由不进行重定向
  const excludedRoutes = ['/welcome', PATH_LOGIN];

  // 如果路径中的语言不支持则重定向
  if (
    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith('/_next') &&
    req.nextUrl.pathname !== '/' &&
    !excludedRoutes.includes(req.nextUrl.pathname)
  ) {
    return NextResponse.redirect(
      new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
    );
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer')!);
    const lngInReferer = languages.find((l) =>
      refererUrl.pathname.startsWith(`/${l}`),
    );
    const response = NextResponse.next();
    if (lngInReferer) response.cookies.set(cookieName, lngInReferer);
    return response;
  }

  return NextResponse.next();
}

export default withLocale;

关键说明

  • matcher合并逻辑:数组形式的matcher会匹配任意符合条件的路径,确保所有需要多语言处理和鉴权的路径都被覆盖。
  • 执行顺序:先处理多语言路径重定向,再执行鉴权,避免鉴权时路径不规范。
  • withAuth正确用法:预先生成authMiddleware实例,再在主中间件中调用,确保鉴权逻辑正常执行。

内容的提问来源于stack exchange,提问作者in43sh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 07:58:12