魯斯前端布魯斯前端

文章中英模式

布魯斯前端面試題目 - 跨域是什麼?怎麼解決跨域問題?

深入解析跨來源資源共用(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. 使用代理伺服器(Proxy)

在前端開發環境中,可以配置代理伺服器轉發請求:

// 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 }
    );
  }
}