HTTP CORS 跨域資源共享問題

HTTP CORS 完全攻略:跨域資源共享問題分析與解決方案

更新日期:2025 年 5 月 18 日

在現代網頁開發中,幾乎每位前端開發者都曾遭遇過這個令人頭痛的錯誤訊息:「Access to fetch at 'https://api.example.com' from origin 'https://example.com' has been blocked by CORS policy」。這個看似神秘的 CORS 錯誤,已成為 Web 開發過程中最常見的障礙之一。但究竟什麼是 CORS?為什麼會發生這些錯誤?又該如何正確解決?本文將深入淺出地剖析 CORS 機制的前因後果,並提供全面的解決方案,幫助開發者徹底掌握跨域資源共享的技術挑戰。

什麼是 CORS?

CORS(Cross-Origin Resource Sharing,跨域資源共享)是一種 HTTP 標頭機制,允許 Web 應用程式在不同網域間安全地共享資源。它擴展了同源政策(Same-Origin Policy)的限制,使得網頁能夠請求不同來源的資源,同時仍保持必要的安全性。

簡單來說,CORS 是瀏覽器的一種安全機制,用來控制一個網站能否載入另一個網域的資源。這是一個前端安全的重要屏障,能夠有效防止惡意網站竊取用戶數據或進行跨站請求偽造攻擊。

同源政策:CORS 的基礎

要理解 CORS,我們必須先了解「同源政策」這個概念。在 Web 安全模型中,瀏覽器會限制網頁發起的跨源 HTTP 請求。所謂「同源」,是指協議、網域名稱和埠號都相同。

https://example.com/page.html 為例,下列情況都被視為「非同源」:

  • 不同協議http://example.com/page.html(https vs http)
  • 不同網域https://api.example.com/data.json(example.com vs api.example.com)
  • 不同埠號https://example.com:8080/page.html(默認 443 vs 8080)

同源政策預設會阻止網頁透過 JavaScript 取得上述「非同源」資源,而 CORS 正是為了在保持安全性的前提下,允許這類跨域請求的機制。

同源政策示意圖

為什麼會發生 CORS 問題?

CORS 問題之所以如此普遍,主要是由於現代 Web 應用的架構特點與瀏覽器安全機制之間的衝突。讓我們深入探討這些原因:

現代 Web 架構的變化

  • 前後端分離:現代 Web 開發通常採用前後端分離架構,前端 UI 和後端 API 往往部署在不同的網域。
  • 微服務架構:一個應用可能調用多個不同網域的微服務 API。
  • 第三方服務整合:許多應用需要整合第三方 API(如支付、社交媒體、地圖服務等)。
  • 靜態資源 CDN:靜態資源通常部署在 CDN 上,與主應用不在同一網域。

瀏覽器安全模型的演進

同源政策是瀏覽器安全的基石,但隨著 Web 應用越來越複雜,這一政策需要適當放寬。CORS 正是為解決這一矛盾而誕生的標準機制。當 CORS 問題發生時,實際上是因為:

  • 瀏覽器執行了保護:瀏覽器檢測到跨域請求,但服務器未正確配置 CORS 頭部。
  • 防止未授權訪問:這一機制旨在防止惡意網站在用戶不知情的情況下訪問敏感信息。
  • 服務端未明確允許跨域:服務器需要明確告訴瀏覽器哪些跨域請求是被允許的。

值得注意的是,CORS 錯誤是純粹的瀏覽器限制。如果使用 Postman 或服務器端代碼發送相同請求,是不會受到 CORS 限制的,因為這些工具不執行瀏覽器的同源政策。這也是為什麼有時候 API 看起來工作正常,但在瀏覽器中卻出現 CORS 錯誤。

CORS 工作原理圖

CORS 的工作流程

CORS 機制的核心在於 HTTP 頭部的交換。當瀏覽器發起跨域請求時:

  1. 請求階段:瀏覽器在 HTTP 請求中添加 Origin 頭部,標明請求的來源。
  2. 回應階段:服務器檢查 Origin 頭部,如果允許該來源的跨域請求,則在響應中添加 Access-Control-Allow-Origin 頭部。
  3. 驗證階段:瀏覽器檢查響應中的 CORS 頭部,如果 Access-Control-Allow-Origin 匹配當前來源(或為萬用字元 *),則允許網頁訪問響應內容。

預檢請求(Preflight Request)

對於某些「非簡單請求」(如使用 PUT/DELETE 方法,或包含自定義頭部的請求),瀏覽器會先發送「預檢請求」:

  • 預檢請求使用 OPTIONS 方法,詢問服務器是否允許實際請求。
  • 服務器必須通過返回適當的 CORS 頭部來回應這一詢問。
  • 只有預檢請求成功,瀏覽器才會發送實際請求。

技術筆記:許多 CORS 問題出現在預檢請求階段,因為後端常常忽略了對 OPTIONS 請求的正確處理。

常見的 CORS 錯誤類型

CORS 錯誤可能以多種形式出現,了解常見錯誤類型有助於我們更快地診斷和解決問題:

缺少 Access-Control-Allow-Origin 頭部

Access to fetch at 'https://api.example.com/data' 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/data' from origin 'https://example.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.

當前端請求包含憑證(如 cookies)但服務器未正確配置 Access-Control-Allow-Credentials 頭部時,會出現此錯誤。

不允許的方法或頭部

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

預檢請求失敗,因為服務器未明確允許請求中使用的 HTTP 方法或頭部字段。

萬用字元與憑證衝突

The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

當請求包含憑證(credentials: 'include')時,服務器不能使用萬用字元 * 作為 Access-Control-Allow-Origin 的值,必須明確指定允許的來源。

如何解決 CORS 問題

解決 CORS 問題通常需要從後端入手,但也有一些前端或架構層面的解決方案。以下是全面的解決方案指南:

後端解決方案

最直接且最推薦的方法是在服務器端正確配置 CORS 頭部:

Node.js/Express 解決方案

使用 cors 中間件:

const express = require('express');
const cors = require('cors');
const app = express();

// 基本用法(允許所有來源)
app.use(cors());

// 或自定義配置
app.use(cors({
  origin: 'https://example.com',  // 允許的來源
  methods: ['GET', 'POST', 'PUT', 'DELETE'],  // 允許的 HTTP 方法
  allowedHeaders: ['Content-Type', 'Authorization'],  // 允許的請求頭
  credentials: true  // 允許攜帶憑證(cookies)
}));

// 特定路由單獨配置
app.get('/api/public-data', cors(), (req, res) => {
  // 處理請求
});

Python/Django 解決方案

使用 django-cors-headers 套件:

# settings.py
INSTALLED_APPS = [
    # ...其他應用
    'corsheaders',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    # 務必放在其他中間件之前
    # ...其他中間件
]

# 允許所有來源
CORS_ALLOW_ALL_ORIGINS = True

# 或指定允許的來源
CORS_ALLOWED_ORIGINS = [
    "https://example.com",
    "https://sub.example.com",
]

# 允許憑證
CORS_ALLOW_CREDENTIALS = True

PHP 解決方案

在 PHP 腳本中添加必要的頭部:

<?php
// 允許特定來源
header("Access-Control-Allow-Origin: https://example.com");
// 或允許所有來源(不建議用於生產環境)
// header("Access-Control-Allow-Origin: *");

// 允許的方法
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");

// 允許的頭部
header("Access-Control-Allow-Headers: Content-Type, Authorization");

// 允許憑證
header("Access-Control-Allow-Credentials: true");

// 預檢請求快取時間(秒)
header("Access-Control-Max-Age: 86400");

// 處理 OPTIONS 預檢請求
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    exit(0);
}

// 實際請求的處理邏輯
// ...
?>

Nginx 代理配置

通過 Nginx 添加 CORS 頭部:

server {
    # ...其他配置
    
    location /api/ {
        # 添加 CORS 頭部
        add_header 'Access-Control-Allow-Origin' 'https://example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Max-Age' '86400';
        
        # 處理 OPTIONS 預檢請求
        if ($request_method = 'OPTIONS') {
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        
        # 反向代理到實際服務
        proxy_pass http://backend-service;
        # ...其他代理配置
    }
}

前端解決方案

雖然 CORS 問題最好在後端解決,但有時候我們無法修改後端代碼(如使用第三方 API)。這時可考慮以下前端解決方案:

根據瀏覽器的同源政策(Same-Origin Policy),前端網頁若嘗試從不同源請求資源,瀏覽器會自動啟動 CORS 機制,要求後端明確允許該來源才能讀取資料。這是一種瀏覽器主動限制「回應存取」的安全防護措施。實際上,請求仍然會被發送出去,伺服器也可能已收到並處理,只是若回應中缺少正確的 CORS 標頭(如 Access-Control-Allow-Origin),瀏覽器便會阻止 JavaScript 存取回應資料。

因此,當我們無法控制第三方伺服器時,只能透過「變更請求來源或繞過瀏覽器限制」的方式來解決。例如使用中介代理伺服器來轉發請求,使得對瀏覽器而言,請求來源仍然是同源的,或是在開發環境中設定本地代理以模擬同源情境。這些做法的核心原理都是:避免讓瀏覽器觸發跨域的安全限制

使用代理伺服器

開發環境中,可利用框架內建的代理功能:

  • Vite:在 vite.config.js 中配置代理
    export default {
      server: {
        proxy: {
          '/api': {
            target: 'https://api.example.com',
            changeOrigin: true,
            rewrite: (path) => path.replace(/^/api/, '')
          }
        }
      }
    }
  • Create React App:在 package.json 中配置代理
    {
      "proxy": "https://api.example.com"
    }

生產環境代理解決方案

生產環境中,可以設置自己的代理服務器:

  • 架設代理服務器:使用 Nginx 或 Node.js 建立代理,將前端請求轉發到目標 API 並添加適當的 CORS 頭部。
  • 雲端代理服務:使用 Cloudflare Workers、AWS Lambda 或其他無伺服器解決方案建立輕量級代理。
  • API 閘道:如果使用雲端架構,可配置 API Gateway 處理 CORS。

注意:代理方案解決的是前端跨域問題,但代理伺服器本身仍需處理與目標 API 的授權認證問題。切勿將敏感憑證暴露在前端代碼中。

JSONP 方案(傳統方案)

JSONP (JSON with Padding) 是一種繞過同源政策的傳統技術,利用 <script> 標籤不受同源限制的特性:

// 前端實現
function jsonpRequest(url, callback) {
  const script = document.createElement('script');
  const callbackName = 'jsonp_' + Math.random().toString(36).substring(2, 15);
  
  // 全局回調函數
  window[callbackName] = function(data) {
    callback(data);
    document.body.removeChild(script);
    delete window[callbackName];
  };
  
  // 添加回調參數到 URL
  script.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + callbackName;
  document.body.appendChild(script);
}

// 使用示例
jsonpRequest('https://api.example.com/data', function(data) {
  console.log('Received data:', data);
});

注意:JSONP 只支援 GET 請求,且有安全隱憂,現代應用中已經很少使用。

架構層面的解決方案

除了直接處理 CORS 問題,有時候我們可以通過調整應用架構來避免跨域問題:

  • 合併部署:將前端應用與 API 部署在同一域名下,避免跨域請求。
  • 子域策略:將 API 部署在主域的子域下(如 api.example.com),並通過適當設置 Cookie 的 domain 來共享會話。
  • BFF 模式:採用 Backend for Frontend 模式,為前端提供專用的 API 層,統一處理跨域問題。
  • 統一網域名稱系統:使用 Nginx 等反向代理,將不同服務統一到同一域名下的不同路徑。

CORS 錯誤是什麼時候發生的?

許多開發者會疑惑:「當前端對不同源的 API 發送請求時,是在請求發出前就被攔截,還是在收到回應時才報錯?」
答案是:請求其實已經發送出去了,但錯誤發生在瀏覽器解析回應時

瀏覽器基於安全考量,會對跨來源請求啟用 CORS 機制。當後端未回傳正確的 CORS 標頭(如 Access-Control-Allow-Origin),即使請求成功送達,瀏覽器也會阻止 JavaScript 存取回應資料,並在開發者工具中報錯。

這種錯誤不是網路傳輸失敗,而是瀏覽器主動封鎖對回應內容的存取。

所以簡單來說:請求發得出去,問題發生在回應階段

最佳實踐與安全考量

在解決 CORS 問題時,應該遵循以下最佳實踐,確保既滿足功能需求,又不犧牲安全性:

CORS 配置最佳實踐

  • 避免使用萬用字元:生產環境中避免使用 Access-Control-Allow-Origin: *,應明確指定允許的來源域名。
  • 僅允許必要的 HTTP 方法:在 Access-Control-Allow-Methods 中只列出應用實際需要的方法。
  • 設置合理的預檢快取時間:通過 Access-Control-Max-Age 設置適當的預檢請求快取時間,減少 OPTIONS 請求。
  • 謹慎處理憑證:只在必要時設置 Access-Control-Allow-Credentials: true,且確保正確配置 Access-Control-Allow-Origin
  • 動態設置來源頭部:根據請求的 Origin 頭部動態設置 Access-Control-Allow-Origin,而不是硬編碼。

安全考量

CORS 是一個安全機制,過於寬鬆的配置可能帶來風險:

  • CSRF 風險:允許憑證跨域可能增加跨站請求偽造 (CSRF) 攻擊的風險,應確保實施其他防護措施。
  • 數據洩露:過於寬鬆的 CORS 配置可能允許惡意站點讀取敏感響應數據。
  • 子域安全:允許所有子域(如 *.example.com)可能存在風險,若其中某個子域被攻擊者控制。
  • 預檢繞過:某些簡單請求不觸發預檢,應確保服務器端也實施適當的身份驗證和授權。

最佳實踐提示:在生產環境中,建議根據實際需要設置嚴格的 CORS 策略,只允許必要的來源、方法和頭部,並定期審查 CORS 配置。

CORS 問題診斷工具

遇到 CORS 問題時,以下工具可幫助快速診斷:

  • 瀏覽器開發者工具:在網絡面板(Network tab)中可查看請求頭部、響應頭部和詳細的 CORS 錯誤。
  • CORS 檢測工具:線上工具如 CORS Test 可幫助測試 API 端點的 CORS 配置。
  • 後端日誌:檢查服務器日誌中 OPTIONS 請求的處理情況。
  • curl 工具:使用 curl 發送帶有 Origin 頭部的請求,檢查服務器響應中的 CORS 頭部。

總結:CORS 不再是難題

CORS 錯誤雖然常見又令人困擾,但理解其背後的原理和解決方案後,處理起來就變得清晰明了。記住以下關鍵點:

  • CORS 是瀏覽器的安全機制,保護用戶免受潛在的跨站攻擊。
  • 最佳解決方案是在後端正確配置 CORS 頭部,特別是 Access-Control-Allow-Origin
  • 當無法修改後端時,代理服務器是一個實用的替代方案。
  • 安全與便利性之間需要平衡,應避免過於寬鬆的 CORS 配置。
  • 現代 Web 框架和雲服務通常提供內建的 CORS 支持,簡化了配置過程。

通過掌握本文介紹的知識和技術,你應該能夠自信地應對開發中遇到的各種 CORS 挑戰,構建出真正無縫整合的現代 Web 應用。

如有其他關於 Web 開發或前後端整合的問題,歡迎繼續關注我們的技術文章系列!

© 2025 一隻河蟹. All rights reserved. | 隱私條款 | 聯絡我們