Next.js中的跨域资源共享(CORS)实现指南 (2)

243 阅读6分钟

了解了CORS的基本原理后,让我们聚焦于如何在Next.js项目中实际解决跨域问题。Next.js提供了多种方式来处理CORS,具体方法取决于你使用的是Pages Router还是App Router,以及特定的应用场景。

针对不同Next.js API的CORS实现

1. Pages Router: API Routes

在Pages Router中,API Routes位于pages/api目录下,需要手动设置CORS头。

基本实现:

// pages/api/data.js
export default function handler(req, res) {
  // 设置CORS头
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // 处理OPTIONS预检请求
  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }

  // 正常处理请求
  if (req.method === 'GET') {
    res.status(200).json({ message: "Success" });
  } else {
    res.status(405).json({ message: "Method not allowed" });
  }
}

使用cors中间件包:

// pages/api/data.js
import Cors from 'cors';

// 初始化CORS中间件
const cors = Cors({
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  origin: '*',
  credentials: true,
});

// 辅助函数执行中间件
function runMiddleware(req, res, fn) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
}

export default async function handler(req, res) {
  // 运行CORS中间件
  await runMiddleware(req, res, cors);

  // 处理请求
  res.status(200).json({ message: "Success" });
}

2. App Router: Route Handlers

App Router引入的Route Handlers使用新的响应API,位于app目录中的route.js|ts文件。

基本实现:

// app/api/data/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
  const response = NextResponse.json({ message: "Success" });
  
  // 设置CORS头
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  return response;
}

// 处理OPTIONS请求
export async function OPTIONS(request: NextRequest) {
  const response = new NextResponse(null, { status: 204 });
  
  response.headers.set('Access-Control-Allow-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'); // 缓存预检结果24小时
  
  return response;
}

多端点复用的辅助函数:

// app/lib/cors.ts
import { NextResponse } from 'next/server';

export function setCorsHeaders(response: NextResponse) {
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  return response;
}

// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { setCorsHeaders } from '@/app/lib/cors';

export async function GET(request: NextRequest) {
  const response = NextResponse.json({ items: ['item1', 'item2'] });
  return setCorsHeaders(response);
}

export async function OPTIONS(request: NextRequest) {
  const response = new NextResponse(null, { status: 204 });
  setCorsHeaders(response);
  response.headers.set('Access-Control-Max-Age', '86400');
  return response;
}

3. Middleware全局CORS处理

使用Middleware是处理CORS的最灵活方式,它可以应用于整个应用或特定路径,不需要在每个API路由中重复代码。

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

export function middleware(request: NextRequest) {
  // 只处理API路由的请求
  if (request.nextUrl.pathname.startsWith('/api/')) {
    // 获取请求的来源
    const origin = request.headers.get('origin') || '';
    
    // 创建响应对象
    const response = NextResponse.next();
    
    // 设置CORS头
    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');
    
    // 处理OPTIONS请求
    if (request.method === 'OPTIONS') {
      return new NextResponse(null, { 
        status: 204,
        headers: response.headers,
      });
    }
    
    return response;
  }
  
  return NextResponse.next();
}

// 配置哪些路径触发中间件
export const config = {
  matcher: '/api/:path*',
};

4. Server Actions与CORS

Next.js 13引入的Server Actions也需要考虑CORS,特别是当从其他域调用时。

Server Actions默认创建公共HTTP端点,可以通过serverActions.allowedOrigins配置允许的来源:

// next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['example.com', '*.example.com'],
    },
  },
}

Server Actions使用POST方法,并通过比较Origin和Host头提供额外保护。确保在Server Action中添加适当的授权检查:

// app/actions.ts
'use server'

import { auth } from './lib';

export async function addItem() {
  // 验证用户是否有权限
  const { user } = auth();
  if (!user) {
    throw new Error('Authentication required');
  }
  
  // 执行操作...
}

常见CORS场景解决方案

1. 处理带凭证的请求

当需要跨域发送Cookie时:

  1. 服务器必须指定具体的允许域名(不能用*
  2. 必须设置Access-Control-Allow-Credentials: true
  3. 客户端必须设置credentials: 'include'
// app/api/auth/route.ts
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  // 身份验证逻辑...
  
  // 设置cookie
  const cookieStore = cookies();
  cookieStore.set('session', 'token123', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7,
    path: '/',
  });
  
  const response = NextResponse.json({ success: true });
  
  // 设置CORS头(注意使用具体域名)
  response.headers.set('Access-Control-Allow-Origin', 'https://example.com');
  response.headers.set('Access-Control-Allow-Credentials', 'true');
  response.headers.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
  
  return response;
}

// 客户端代码需要设置 credentials: 'include'
// fetch('https://api.myapp.com/api/auth', {
//   method: 'POST',
//   credentials: 'include',
//   ...
// })

2. 动态设置允许的来源

在生产环境中,通常需要限制允许的来源域名:

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

// 允许的来源列表
const allowedOrigins = [
  'https://example.com',
  'https://www.example.com',
  'https://app.example.com',
];

// 开发环境添加localhost
if (process.env.NODE_ENV === 'development') {
  allowedOrigins.push('http://localhost:3000');
}

export function middleware(request: NextRequest) {
  const origin = request.headers.get('origin') || '';
  
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const isAllowedOrigin = allowedOrigins.includes(origin);
    const response = NextResponse.next();
    
    if (isAllowedOrigin) {
      // 设置具体的域名(而非通配符)
      response.headers.set('Access-Control-Allow-Origin', origin);
      // 其他CORS头...
    } else {
      // 对于不在允许列表中的域名,仍然返回响应但不设置CORS头
      // 这会导致浏览器阻止这些域的请求
    }
    
    return response;
  }
  
  return NextResponse.next();
}

3. 设置全局CORS头部

如果你想在全局范围内设置CORS头,可以使用next.config.jsheaders选项:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        // 匹配所有API路由
        source: "/api/:path*",
        headers: [
          { key: "Access-Control-Allow-Credentials", value: "true" },
          { key: "Access-Control-Allow-Origin", value: "*" }, // 或者特定域名
          { key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT,OPTIONS" },
          { key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization" }
        ]
      }
    ]
  }
};

这种方法适用于Pages Router和App Router,但请注意,它无法动态响应不同的Origin头。

从外部服务获取数据的CORS处理

当Next.js服务器需要请求外部API时,记住服务器到服务器的通信不受CORS限制。你可以使用以下模式来避免前端CORS问题:

使用API Routes或Route Handlers作为代理

// app/api/external-data/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  // 服务器端请求不受CORS限制
  const response = await fetch('https://external-api.com/data', {
    headers: {
      // 可以安全地添加API密钥,因为这个代码在服务器上运行
      'Authorization': `Bearer ${process.env.API_KEY}`
    }
  });
  
  const data = await response.json();
  
  // 向客户端返回数据,无需CORS问题
  return NextResponse.json(data);
}

使用Server Components获取数据

// app/dashboard/page.tsx - 服务器组件
import { fetchDashboardData } from '@/lib/api';

export default async function DashboardPage() {
  // 在服务器组件中获取数据不会有CORS问题
  const data = await fetchDashboardData();
  
  return (
    <div>
      <h1>Dashboard</h1>
      {/* 使用数据渲染UI */}
    </div>
  );
}

故障排除

如果你遇到CORS问题,检查以下几点:

  1. 确保添加了OPTIONS方法处理: 跨域的复杂请求需要正确响应预检请求
  2. 检查Origin匹配: 确保指定的Access-Control-Allow-Origin与请求头中的Origin完全匹配(包括协议、子域名和端口)
  3. 凭证请求特殊要求: 使用credentials: 'include'时,Access-Control-Allow-Origin不能是*
  4. Headers值验证: 确保Access-Control-Allow-Headers包含请求中使用的所有自定义头
  5. 方法验证: 确保Access-Control-Allow-Methods包含使用的HTTP方法
  6. 缓存设置: 使用Access-Control-Max-Age减少预检请求频率
  7. 中间件与路由冲突: 避免在中间件和路由处理器中设置冲突的CORS头

更多阅读 # 跨域资源共享(CORS)完全指南:从同源策略到解决方案 (1)