07. 中间件 - Middleware

概述

Middleware 是在请求完成前执行的代码,可以拦截请求、修改响应、实现身份验证、重定向、日志记录等功能。它在 Next.js 中运行在边缘环境,性能极高。

维度 内容
What 在请求完成前执行的代码,可修改响应
Why 实现身份验证、重定向、请求日志等
When 请求处理前需要执行逻辑时
Where 项目根目录的 middleware.js 文件
Who 需要请求拦截的开发者
How export function middleware(request) { if (!request.cookies.get('token')) return NextResponse.redirect('/login') }

1. Middleware 基础

1.1 创建 Middleware

// middleware.js (项目根目录)
import { NextResponse } from 'next/server';

export function middleware(request) {
  // 中间件逻辑
  console.log('请求路径:', request.nextUrl.pathname);
  
  return NextResponse.next(); // 继续处理请求
}

// 配置匹配路径
export const config = {
  matcher: '/about/:path*',
};

1.2 基础示例

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  // 获取当前路径
  const { pathname } = request.nextUrl;
  
  // 记录日志
  console.log(`[${new Date().toISOString()}] ${request.method} ${pathname}`);
  
  // 添加自定义请求头
  const response = NextResponse.next();
  response.headers.set('X-Custom-Header', 'middleware-added');
  
  return response;
}

// 匹配所有路径
export const config = {
  matcher: '/:path*',
};

2. 路径匹配

2.1 单个路径

// middleware.js
export const config = {
  matcher: '/about',
};

2.2 多个路径

// middleware.js
export const config = {
  matcher: ['/about', '/contact', '/dashboard/:path*'],
};

2.3 排除路径

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

2.4 正则匹配

// middleware.js
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
  ],
};

3. 身份验证

3.1 基于 Cookie 的认证

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const { pathname } = request.nextUrl;
  
  // 公开路径
  const publicPaths = ['/login', '/register', '/'];
  const isPublicPath = publicPaths.includes(pathname);
  
  // 获取 token
  const token = request.cookies.get('token')?.value;
  const isAuthenticated = !!token;
  
  // 未认证且访问私有路径 → 重定向到登录页
  if (!isAuthenticated && !isPublicPath) {
    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: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
    '/login',
    '/register',
  ],
};

3.2 基于 Session 的认证

// middleware.js
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(request) {
  const token = await getToken({ req: request });
  const { pathname } = request.nextUrl;
  
  // 需要认证的路由
  const authRoutes = ['/dashboard', '/profile', '/settings'];
  const isAuthRoute = authRoutes.some(route => pathname.startsWith(route));
  
  if (isAuthRoute && !token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }
  
  // 管理员路由
  if (pathname.startsWith('/admin')) {
    if (!token || token.role !== 'admin') {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }
  
  return NextResponse.next();
}

3.3 JWT 验证

// middleware.js
import { NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';

export function middleware(request) {
  const token = request.cookies.get('jwt')?.value;
  const { pathname } = request.nextUrl;
  
  // 验证 JWT
  if (pathname.startsWith('/api/protected')) {
    try {
      jwt.verify(token, process.env.JWT_SECRET);
    } catch (error) {
      return NextResponse.json(
        { error: '未授权' },
        { status: 401 }
      );
    }
  }
  
  return NextResponse.next();
}

4. 重定向和重写

4.1 重定向

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const { pathname } = request.nextUrl;
  
  // 旧路径重定向到新路径
  if (pathname === '/old-about') {
    return NextResponse.redirect(new URL('/about', request.url));
  }
  
  // 永久重定向 (301)
  if (pathname === '/old-blog') {
    return NextResponse.redirect(new URL('/blog', request.url), { status: 301 });
  }
  
  // 临时重定向 (307)
  if (pathname === '/maintenance') {
    return NextResponse.redirect(new URL('/maintenance.html', request.url), { status: 307 });
  }
  
  return NextResponse.next();
}

4.2 重写

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const { pathname } = request.nextUrl;
  
  // A/B 测试:随机显示不同页面
  if (pathname === '/landing') {
    const isVariant = Math.random() < 0.5;
    const url = request.nextUrl.clone();
    url.pathname = isVariant ? '/landing-variant' : '/landing';
    return NextResponse.rewrite(url);
  }
  
  // 国际化:根据 cookie 重写路径
  const locale = request.cookies.get('locale')?.value || 'zh';
  if (pathname === '/') {
    const url = request.nextUrl.clone();
    url.pathname = `/${locale}`;
    return NextResponse.rewrite(url);
  }
  
  return NextResponse.next();
}

4.3 地理重定向

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const country = request.geo?.country || 'US';
  const { pathname } = request.nextUrl;
  
  // 根据国家重定向
  if (pathname === '/') {
    if (country === 'CN') {
      return NextResponse.redirect(new URL('/cn', request.url));
    }
    if (country === 'JP') {
      return NextResponse.redirect(new URL('/jp', request.url));
    }
  }
  
  return NextResponse.next();
}

5. 请求和响应修改

5.1 添加请求头

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const response = NextResponse.next();
  
  // 添加响应头
  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');
  
  // 设置 Cookie
  response.cookies.set('visited', 'true', { maxAge: 60 * 60 * 24 });
  
  // 删除 Cookie
  response.cookies.delete('temp');
  
  return response;
}

5.2 修改请求

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const requestHeaders = new Headers(request.headers);
  
  // 添加自定义请求头
  requestHeaders.set('x-user-id', '123');
  requestHeaders.set('x-request-time', Date.now().toString());
  
  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

6. 请求日志和监控

6.1 请求日志

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const { method, url, headers } = request;
  const userAgent = headers.get('user-agent');
  const ip = headers.get('x-forwarded-for') || 'unknown';
  
  console.log({
    timestamp: new Date().toISOString(),
    method,
    url,
    userAgent,
    ip,
  });
  
  return NextResponse.next();
}

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

6.2 性能监控

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const start = Date.now();
  
  const response = NextResponse.next();
  
  response.headers.set('X-Response-Time', `${Date.now() - start}ms`);
  
  return response;
}

7. 完整示例:认证中间件

// middleware.js
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

// 公开路径(不需要认证)
const publicPaths = [
  '/',
  '/login',
  '/register',
  '/about',
  '/contact',
  '/api/auth',
];

// 角色权限映射
const rolePermissions = {
  user: ['/dashboard', '/profile'],
  admin: ['/dashboard', '/profile', '/admin', '/settings'],
};

// 检查路径是否需要认证
function isPublicPath(pathname) {
  return publicPaths.some(path => pathname === path || pathname.startsWith(`${path}/`));
}

// 检查用户是否有权限访问路径
function hasPermission(pathname, role) {
  const allowedPaths = rolePermissions[role] || [];
  return allowedPaths.some(path => pathname === path || pathname.startsWith(`${path}/`));
}

export async function middleware(request) {
  const { pathname } = request.nextUrl;
  
  // 1. 跳过公开路径
  if (isPublicPath(pathname)) {
    return NextResponse.next();
  }
  
  // 2. 获取用户 token
  const token = await getToken({ req: request });
  
  // 3. 未认证 → 重定向到登录页
  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }
  
  // 4. 检查角色权限
  if (!hasPermission(pathname, token.role)) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }
  
  // 5. 添加用户信息到请求头
  const response = NextResponse.next();
  response.headers.set('x-user-id', token.sub);
  response.headers.set('x-user-role', token.role);
  
  return response;
}

// 配置匹配路径
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
    '/admin/:path*',
    '/api/protected/:path*',
  ],
};

8. 总结

核心要点

要点 说明
位置 项目根目录 middleware.js
运行环境 边缘运行时(Edge Runtime)
主要用途 认证、重定向、日志、A/B 测试
配置 config.matcher 匹配路径

常用模式

场景 实现方式
身份验证 检查 token,无则重定向
重定向 NextResponse.redirect()
重写 URL NextResponse.rewrite()
添加头 response.headers.set()
设置 Cookie response.cookies.set()

记忆口诀

中间件在根目录,请求拦截好帮手
认证重定向都能做,路径匹配配置走
边缘运行速度快,性能提升不用愁


9. 相关资源


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐