鲁斯前端布鲁斯前端

文章中英模式

布鲁斯前端面试题目 - 跨域是什么?怎么解决跨域问题?

深入解析跨来源资源共享(CORS)的工作原理、常见错误与解决方案。掌握前端面试中关于跨域的核心知识,包含各种解决跨域问题的方法。

影片縮圖

懒得看文章?那就来看视频吧

什么是跨域(CORS)?

跨来源资源共享(Cross-Origin Resource Sharing, CORS)是一种浏览器安全机制,用于控制不同来源(网域、协议或端口)之间的资源请求。简单来说,它就像是网站间的「边境管制」,决定哪些外部资源可以被访问。

举个例子:假设你正在浏览 https://shopping.com 的网站,而这个网站需要从 https://api.shopping.com 获取商品数据。虽然它们看起来很相似,但因为域名不同,浏览器会视为「跨域请求」,默认会阻止这种访问。

这就像你在一家商店(前端网站)想要取得另一家商店(API服务器)的商品,但需要对方明确同意才行。如果API服务器没有说「我允许shopping.com访问我的资源」,浏览器就会挡下这个请求。

⚠️ 同源的定义:

两个URL必须具有相同的协议(http/https)、域名和端口才被视为同源。就像住在同一栋大楼的邻居。

  • 1. https://example.com 和 https://api.example.com 不同源(子域名不同,像是同一条街不同栋大楼)
  • 2. http://example.com 和 https://example.com 不同源(协议不同,一个用走路一个用开车到同一地点)
  • 3. https://example.com 和 https://example.com:8080 不同源(端口不同,像是同一栋大楼不同楼层)

CORS的工作机制

CORS有两种主要请求类型:简单请求和预检请求。

类型特点流程使用场景
简单请求• GET/HEAD/POST方法
• 基本头部
• 简单Content-Type
直接发送请求,带Origin头部简单表单提交、基础GET请求
预检请求
(主流用法)
• 特殊HTTP方法
• 自定义头部
• JSON等特殊Content-Type
1. 先发OPTIONS请求询问
2. 服务器响应许可
3. 再发实际请求
现代API请求、JSON数据交换、带认证的请求

在现代Web开发中,预检请求是主流情况,因为大多数API请求都使用JSON格式和自定义头部,这些都会触发预检机制。

// 预检请求
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST                // 即使是简单方法(POST)
Access-Control-Request-Headers: Content-Type, X-Custom-Header  // 但有自定义头部触发预检

// 预检响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400  // 预检结果缓存时间(秒)

常见CORS错误

Access to fetch at 'https://api.example.com' from origin 'https://example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

最常见的CORS错误,表示服务器未设置Access-Control-Allow-Origin头部。

Access to fetch at 'https://api.example.com' from origin 'https://example.com' has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

服务器未在Access-Control-Allow-Methods中允许请求的HTTP方法。

Access to fetch at 'https://api.example.com' from origin 'https://example.com' has been blocked by CORS policy: Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers in preflight response.

服务器未在Access-Control-Allow-Headers中允许请求的自定义头部。

解决跨域问题的方法

1. 后端配置CORS头部

最标准的解决方案是在后端服务器正确配置CORS头部:

// Node.js Express示例
const express = require('express');
const app = express();

// 配置CORS中间件
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://example.com'); // 或使用 * 允许所有来源
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true'); // 允许携带凭证
  
  // 处理预检请求
  if (req.method === 'OPTIONS') {
    return res.status(204).end();
  }
  
  next();
});

// 或使用cors套件
const cors = require('cors');
app.use(cors({
  origin: 'https://example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true
}));

2. 使用代理服务器

在前端开发环境中,可以配置代理服务器转发请求:

// webpack开发服务器代理配置
module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
}

// Next.js代理配置
// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://api.example.com/:path*',
      },
    ]
  },
}

3. 使用后端中继服务器

在Next.js 15中使用Route Handlers作为中继服务器转发请求:

这种方法有效是因为同源策略只适用于浏览器到服务器的请求,而服务器之间的请求不受CORS限制。

当我们的Next.js应用作为中继服务器时,浏览器只与同源的Next.js服务器通信,然后由Next.js服务器转发请求到第三方API并将响应返回给前端,从而绕过了CORS限制。

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

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  
  try {
    // 从第三方API获取数据
    const response = await fetch(
      `https://api.example.com/data?${searchParams}`
    );
    
    const data = await response.json();
    
    // 返回数据给前端
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch data' },
      { status: 500 }
    );
  }
}

4. 其他解决方案

  • JSONP (仅适用于GET请求):利用script标签不受同源策略限制的特性,但只支持GET请求且安全性较低。
  • 浏览器扩展:开发时可使用CORS浏览器扩展暂时禁用CORS,但仅适用于开发环境。
  • 反向代理:使用Nginx、Apache等反向代理服务器转发请求。

CORS最佳实践

1. 安全性优先

  • 避免使用 * 允许所有来源,明确指定允许的域名
  • 只允许必要的HTTP方法和头部
  • 谨慎使用Access-Control-Allow-Credentials

2. 性能优化

  • 设置适当的Access-Control-Max-Age缓存预检结果
  • 避免不必要的预检请求,使用简单请求格式

3. 开发便利性

  • 开发环境可使用代理或浏览器扩展
  • 生产环境必须正确配置CORS头部

🔥 常见面试题目

(一) 什么是CORS? 为什么浏览器需要同源政策?

解答:CORS是浏览器的安全机制,允许服务器声明哪些网站可以访问它的资源。同源政策就像网站间的防火墙,限制不同网站间的数据存取。

没有同源政策的危险:

恶意网站(evil.com)         用户的银行网站(bank.com)
      |                           |
      |  用户访问恶意网站          |
      |  同时已登录银行网站        |
      |                           |
      |-- 尝试读取银行资料 --X     |
      |                           |
      |   浏览器阻止请求           |
      |   (同源政策保护)           |

如果没有同源政策,当你登录银行网站后,再访问恶意网站时,恶意网站可能会偷偷读取你的银行资料或执行转账等操作。CORS则是在需要跨域访问时,提供安全的标准机制。

(二) 解决CORS问题的最佳方法有哪些? 各有什么优缺点?

解答:解决CORS的主要方法:

方法优点缺点
后端配置CORS标头标准方案,配置灵活需要后端配合
代理服务器前端可独立解决增加请求复杂度
后端中继服务器完全控制请求处理需维护额外服务

最理想的解决方案是后端正确配置CORS标头。如果无法修改API源,则考虑使用代理或中继服务器。

(三) 在前后端分离的开发环境中如何处理CORS问题?

解答:前后端分离开发环境中的CORS解决方案:

1. 开发环境代理:

// Vite (vite.config.js/ts)
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, '')
      }
    }
  }
});

2. 后端开发环境配置:在开发环境允许localhost域名访问

3. 中间代理服务器:如果前端有使用Next.js,可以考虑使用Next.js的代理功能。

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

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  
  try {
    // 从第三方API获取数据
    const response = await fetch(
      `https://api.example.com/data?${searchParams}`
    );
    
    const data = await response.json();
    
    // 返回数据给前端
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch data' },
      { status: 500 }
    );
  }
}