mcpField
N
nextjs-middleware

nextjs-middleware

github.com/josiahsiegel开发5977 次安装19 周前更新
代码仓库

关于 nextjs-middleware

这是用于实现 Next.js Middleware 的参考技能,可在 Edge 处理请求,涵盖路由保护、重定向、重写、请求头、Cookie 和基于请求的路由。

主要功能

  1. 01配置路径匹配规则
  2. 02按身份验证和角色保护路由
  3. 03执行重定向与 URL 重写
  4. 04管理请求头和 Cookie
  5. 05按语言、地理位置或 IP 路由

适用场景

  1. 01限制仪表板和管理路由访问
  2. 02重定向旧路径或重写 URL
  3. 03添加安全与 CORS 请求头
  4. 04按访客位置选择语言或路由

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill nextjs-middleware

安装后在 Claude Code 中输入/nextjs-middleware确认已加载。

详细介绍

快速参考

响应类型代码用途
继续NextResponse.next()直接传递
重定向NextResponse.redirect(url)307/308 重定向
重写NextResponse.rewrite(url)URL 重写(不重定向)
JSONNextResponse.json(data)返回 JSON 响应
匹配器模式匹配内容
'/about'精确路径
'/dashboard/:path*'Dashboard 及所有子路径
'/((?!api|_next).*')除 api 和 _next 之外的所有内容
请求数据访问方式
路径request.nextUrl.pathname
搜索参数request.nextUrl.searchParams
Cookierequest.cookies.get('name')
标头request.headers.get('name')
地理位置request.geo?.country
IPrequest.ip

何时使用此技能

用于Edge 请求处理:

  • 使用身份验证保护路由
  • 实现基于角色的访问控制
  • 设置安全标头(CSP、CORS)
  • 国际化和区域设置检测
  • 使用重写进行 A/B 测试
  • 基于地理位置的路由

相关技能:

  • 身份验证:参见 nextjs-authentication
  • 路由:参见 nextjs-routing-advanced
  • 部署:参见 nextjs-deployment

Next.js 中间件

基础中间件

创建中间件
// middleware.ts (in project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Add custom header
  const response = NextResponse.next();
  response.headers.set('x-custom-header', 'my-value');
  return response;
}

// Configure which paths middleware runs on
export const config = {
  matcher: '/api/:path*',
};
匹配器配置
// Single path
export const config = {
  matcher: '/about',
};

// Multiple paths
export const config = {
  matcher: ['/about', '/dashboard/:path*'],
};

// Regex pattern
export const config = {
  matcher: [
    // Match all paths except static files and images
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

// All paths except API and static
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

身份验证

保护路由
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(request: NextRequest) {
  const token = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });

  const isAuthPage =
    request.nextUrl.pathname.startsWith('/login') ||
    request.nextUrl.pathname.startsWith('/register');

  if (isAuthPage) {
    if (token) {
      // Redirect to dashboard if already logged in
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
    return NextResponse.next();
  }

  if (!token) {
    // Redirect to login if not authenticated
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/login', '/register'],
};
基于角色的访问
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

const roleBasedRoutes: Record<string, string[]> = {
  '/admin': ['admin'],
  '/dashboard': ['admin', 'user'],
  '/settings': ['admin', 'user'],
};

export async function middleware(request: NextRequest) {
  const token = await getToken({ req: request });
  const { pathname } = request.nextUrl;

  // Check role-based access
  for (const [route, allowedRoles] of Object.entries(roleBasedRoutes)) {
    if (pathname.startsWith(route)) {
      if (!token) {
        return NextResponse.redirect(new URL('/login', request.url));
      }

      const userRole = token.role as string;
      if (!allowedRoles.includes(userRole)) {
        return NextResponse.redirect(new URL('/unauthorized', request.url));
      }
    }
  }

  return NextResponse.next();
}

重定向

简单重定向
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const redirects: Record<string, string> = {
  '/old-page': '/new-page',
  '/blog/old-post': '/articles/new-post',
  '/legacy': 'https://legacy.example.com',
};

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

  if (redirects[pathname]) {
    const destination = redirects[pathname];

    // External redirect
    if (destination.startsWith('http')) {
      return NextResponse.redirect(destination);
    }

    // Internal redirect
    return NextResponse.redirect(new URL(destination, request.url));
  }

  return NextResponse.next();
}
条件重定向
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname, searchParams } = request.nextUrl;

  // Redirect old search format
  if (pathname === '/search' && searchParams.has('query')) {
    const query = searchParams.get('query');
    return NextResponse.redirect(new URL(`/search/${query}`, request.url));
  }

  // Redirect trailing slashes
  if (pathname !== '/' && pathname.endsWith('/')) {
    return NextResponse.redirect(
      new URL(pathname.slice(0, -1), request.url)
    );
  }

  return NextResponse.next();
}

重写

URL 重写
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

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

  // Rewrite /api to external API
  if (pathname.startsWith('/api/external')) {
    return NextResponse.rewrite(
      new URL(pathname.replace('/api/external', ''), 'https://api.example.com')
    );
  }

  // A/B testing rewrite
  const bucket = request.cookies.get('bucket')?.value || 'a';
  if (pathname === '/landing') {
    return NextResponse.rewrite(
      new URL(`/landing-${bucket}`, request.url)
    );
  }

  return NextResponse.next();
}
国际化
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const locales = ['en', 'es', 'fr', 'de'];
const defaultLocale = 'en';

function getLocale(request: NextRequest): string {
  // Check cookie
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && locales.includes(cookieLocale)) {
    return cookieLocale;
  }

  // Check Accept-Language header
  const acceptLanguage = request.headers.get('accept-language');
  if (acceptLanguage) {
    const preferredLocale = acceptLanguage
      .split(',')
      .map((lang) => lang.split(';')[0].trim().split('-')[0])
      .find((lang) => locales.includes(lang));

    if (preferredLocale) {
      return preferredLocale;
    }
  }

  return defaultLocale;
}

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

  // Check if pathname has a locale
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) return NextResponse.next();

  // Redirect to localized path
  const locale = getLocale(request);
  return NextResponse.redirect(
    new URL(`/${locale}${pathname}`, request.url)
  );
}

export const config = {
  matcher: ['/((?!_next|api|favicon.ico).*)'],
};

标头

添加标头
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Security headers
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  );

  // CSP header
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
  );

  return response;
}
CORS 标头
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const allowedOrigins = ['https://example.com', 'https://app.example.com'];

export function middleware(request: NextRequest) {
  const origin = request.headers.get('origin');
  const isAllowedOrigin = origin && allowedOrigins.includes(origin);

  // Handle preflight requests
  if (request.method === 'OPTIONS') {
    const response = new NextResponse(null, { status: 204 });

    if (isAllowedOrigin) {
      response.headers.set('Access-Control-Allow-Origin', origin);
    }

    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    response.headers.set('Access-Control-Max-Age', '86400');

    return response;
  }

  const response = NextResponse.next();

  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin);
  }

  return response;
}

export const config = {
  matcher: '/api/:path*',
};

Cookie

设置 Cookie
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Set cookie if not exists
  if (!request.cookies.has('visitor_id')) {
    const visitorId = crypto.randomUUID();
    response.cookies.set('visitor_id', visitorId, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 365, // 1 year
    });
  }

  return response;
}
读取 Cookie
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  const theme = request.cookies.get('theme')?.value || 'light';

  // Use cookie values
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  const response = NextResponse.next();
  response.headers.set('x-theme', theme);

  return response;
}

地理位置和 IP

基于地理位置的路由
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US';
  const city = request.geo?.city || 'Unknown';
  const region = request.geo?.region || 'Unknown';

  // Block certain countries
  const blockedCountries = ['XX', 'YY'];
  if (blockedCountries.includes(country)) {
    return NextResponse.redirect(new URL('/blocked', request.url));
  }

  // Add geo info to headers
  const response = NextResponse.next();
  response.headers.set('x-user-country', country);
  response.headers.set('x-user-city', city);

  return response;
}
基于 IP 的速率限制
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const rateLimit = new Map<string, { count: number; resetTime: number }>();

export function middleware(request: NextRequest) {
  const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
  const now = Date.now();
  const windowMs = 60 * 1000; // 1 minute
  const maxRequests = 100;

  const record = rateLimit.get(ip);

  if (!record || now > record.resetTime) {
    rateLimit.set(ip, { count: 1, resetTime: now + windowMs });
    return NextResponse.next();
  }

  if (record.count >= maxRequests) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  record.count++;
  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

Bot 检测

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

const botPatterns = [
  /bot/i,
  /spider/i,
  /crawl/i,
  /slurp/i,
  /mediapartners/i,
];

export function middleware(request: NextRequest) {
  const userAgent = request.headers.get('user-agent') || '';
  const isBot = botPatterns.some((pattern) => pattern.test(userAgent));

  const response = NextResponse.next();
  response.headers.set('x-is-bot', isBot ? 'true' : 'false');

  // Optionally block certain bots
  if (isBot && request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  return response;
}

最佳实践

实践说明
保持中间件快速每个匹配的请求都会运行
使用 Edge Runtime中间件默认在 Edge 上运行
避免繁重计算不要在中间件中查询数据库
使用具体的匹配器不要对静态文件运行
妥善处理错误始终返回响应
尽可能缓存使用 Cookie/标头保存状态

评论

支持 Markdown