2025年6月11日 星期三

什麼是 CORS?如何在 PHP 中處理跨域請求?

 身為一位資深的 PHP 後端工程師,我來為您詳細解釋什麼是 CORS,以及如何在 PHP 中正確地處理跨域請求。


什麼是 CORS? (Cross-Origin Resource Sharing 跨域資源共享)

CORS 是一種基於 HTTP 標頭的機制,它允許伺服器指示除了其自身來源(網域、協定或端口)之外的任何來源,瀏覽器應該允許從中載入資源。簡而言之,CORS 是瀏覽器實施的同源策略 (Same-Origin Policy) 的一種安全擴展,它提供了一種受控的方式來放寬限制。

同源策略:

這是一個瀏覽器層面的安全機制,它限制了從一個源(例如 https://example.com)載入的網頁,不能與另一個不同源(例如 https://api.example.org 或 http://example.com 或 https://example.com:8080)的資源進行互動。這是為了防止惡意的網站從其他網站竊取用戶數據。

為什麼需要 CORS?

在現代 Web 開發中,前端應用程式(通常運行在一個網域)經常需要呼叫後端 API(可能運行在另一個網域或子網域)。例如:

  • 你的前端 React 應用程式運行在 http://localhost:3000,但後端 PHP API 運行在 http://api.yourdomain.com
  • 你的網站 https://www.yourdomain.com 需要呼叫 https://api.yourdomain.com 提供的數據。
  • 你使用了第三方服務的 API,例如支付網關或地圖服務。

在沒有 CORS 的情況下,瀏覽器會因為同源策略而阻止這些跨域請求,導致前端無法正常獲取數據,進而在開發中遇到著名的 "CORS policy" 錯誤。CORS 就是為了解決這個問題而設計的。

CORS 的工作原理:

CORS 的核心在於伺服器在 HTTP 回應中發送特殊的 CORS 相關 HTTP 標頭,告知瀏覽器該資源是否允許來自特定來源的請求。瀏覽器會根據這些標頭來判斷是否允許將響應內容暴露給前端 JavaScript。

CORS 請求分為兩種類型:

  1. 簡單請求 (Simple Requests):

    如果一個跨域請求滿足以下所有條件,它被認為是簡單請求:

    • 方法為 GETHEADPOST
    • 除了由使用者代理自動設置的標頭(如 User-Agent)和在 Fetch 規範中被定義為 CORS 安全清單的標頭(如 AcceptAccept-LanguageContent-LanguageContent-Type,但 Content-Type 僅限於 application/x-www-form-urlencodedmultipart/form-datatext/plain)之外,沒有額外的手動設置標頭。
    • 沒有使用 ReadableStream 物件發送請求。

    對於簡單請求,瀏覽器會直接發送請求,並在伺服器回應時檢查 Access-Control-Allow-Origin 標頭。

  2. 預檢請求 (Preflight Requests):

    如果一個跨域請求不符合簡單請求的條件(例如,使用了 PUT、DELETE 方法,或者使用了自定義的 Content-Type,或者自定義標頭),瀏覽器會先發送一個 OPTIONS 請求到目標伺服器。這個 OPTIONS 請求被稱為「預檢請求」,它會帶有一些特殊的標頭(如 Access-Control-Request-Method 和 Access-Control-Request-Headers),詢問伺服器是否允許隨後的實際請求。

    伺服器接收到 OPTIONS 請求後,會根據其 CORS 配置回應相應的 CORS 標頭。如果伺服器的回應表明允許該實際請求,瀏覽器才會發送真正的請求。如果預檢請求失敗,瀏覽器會直接阻止實際請求。


如何在 PHP 中處理跨域請求?

在 PHP 中處理 CORS 主要是透過在伺服器端設置正確的 HTTP 響應標頭。這些標頭必須在任何內容輸出到瀏覽器之前發送。

以下是一些常見的 CORS 相關 HTTP 標頭及其在 PHP 中的設置方法:

1. 設置 Access-Control-Allow-Origin (最重要)

這個標頭指定了哪些來源被允許訪問你的資源。

  • 允許所有來源(不推薦用於生產環境):

    PHP
    <?php
    header('Access-Control-Allow-Origin: *');
    // 注意:如果設置為 '*',並且同時需要發送 Credentials (如 cookies, HTTP authentication),瀏覽器會阻止請求。
    // 因為這會帶來安全風險。
    ?>
    

    使用 * 應當極為謹慎,尤其當你的 API 處理敏感數據時。這意味著任何網站都可以向你的 API 發送請求。

  • 允許特定來源(推薦):

    PHP
    <?php
    // 允許來自 'http://localhost:3000' 和 'https://yourfrontend.com' 的請求
    $allowedOrigins = ['http://localhost:3000', 'https://yourfrontend.com'];
    
    if (isset($_SERVER['HTTP_ORIGIN']) && in_array($_SERVER['HTTP_ORIGIN'], $allowedOrigins)) {
        header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
    } else {
        // 如果來源不在允許列表中,你可以選擇不發送 CORS 標頭,讓瀏覽器阻止請求
        // 或者直接拒絕請求並返回錯誤
        // header('HTTP/1.1 403 Forbidden');
        // exit('Forbidden');
    }
    ?>
    

    這是最安全和推薦的做法。動態檢查請求的 Origin 標頭,並只允許受信任的來源。

2. 設置 Access-Control-Allow-Methods

這個標頭用於預檢請求 (OPTIONS) 的回應中,告知瀏覽器實際請求允許使用哪些 HTTP 方法。

PHP
<?php
// 允許 GET, POST, PUT, DELETE, OPTIONS 方法
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
?>

3. 設置 Access-Control-Allow-Headers

這個標頭也用於預檢請求的回應中,告知瀏覽器實際請求允許攜帶哪些自定義的 HTTP 請求頭。

PHP
<?php
// 允許 Content-Type, Authorization, X-Requested-With 等標頭
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
?>

4. 設置 Access-Control-Allow-Credentials

如果你的跨域請求需要攜帶憑證(如 Cookies、HTTP 認證頭或客戶端 SSL 憑證),你需要在伺服器端設置此標頭為 true

PHP
<?php
header('Access-Control-Allow-Credentials: true');
?>

重要注意事項:

  • Access-Control-Allow-Credentialstrue 時,Access-Control-Allow-Origin 不能設置為 *。它必須是一個明確的來源。這是為了安全考量。
  • 前端在發送請求時也需要設置 withCredentials = true (對於 XMLHttpRequest) 或 credentials: 'include' (對於 fetch API)。

5. 設置 Access-Control-Max-Age

這個標頭指示瀏覽器可以緩存預檢請求的結果,在指定的時間內(以秒為單位),瀏覽器不需要再發送預檢請求。這可以減少不必要的網路往返,提高效能。

PHP
<?php
// 緩存預檢請求結果 1 天 (86400 秒)
header('Access-Control-Max-Age: 86400');
?>

6. 處理 OPTIONS 預檢請求

當瀏覽器發送預檢請求時,它的 HTTP 方法是 OPTIONS。你的 PHP 腳本需要特別處理這種請求,只需發送 CORS 相關標頭,然後直接退出,不需要執行實際的業務邏輯。

PHP
<?php
// 這是一個通用的 CORS 處理模板
if (isset($_SERVER['HTTP_ORIGIN'])) {
    // 您可以將此替換為允許的來源列表,例如:
    $allowedOrigins = ['http://localhost:3000', 'https://yourfrontend.com'];
    if (in_array($_SERVER['HTTP_ORIGIN'], $allowedOrigins)) {
        header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
    } else {
        // 如果來源不允許,可以選擇不回應 CORS 標頭或直接退出
        header('HTTP/1.1 403 Forbidden');
        exit;
    }
} else {
    // 可能是非瀏覽器請求,或者沒有 Origin 標頭,根據您的需求處理
    // 為了安全,如果沒有 Origin 標頭,通常不應該允許跨域
}

header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Allow-Credentials: true'); // 如果需要憑證
header('Access-Control-Max-Age: 86400'); // 緩存預檢請求 1 天

// 如果是 OPTIONS 請求,直接退出,不執行後續業務邏輯
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    header('Content-Length: 0'); // OPTIONS 請求通常沒有響應體
    header('Content-Type: text/plain'); // 或者其他適當的 Content-Type
    exit;
}

// -----------------------------------------------------------
// 以下是您的實際業務邏輯處理代碼
// -----------------------------------------------------------

// 範例:處理 GET 請求
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    echo json_encode(['message' => 'Hello from PHP API!']);
}
// 範例:處理 POST 請求
else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $data = json_decode(file_get_contents('php://input'), true);
    echo json_encode(['received_data' => $data, 'status' => 'success']);
}

?>

最佳實踐與注意事項

  • 最小權限原則: 永遠只允許最少的必要權限。不要無腦地設置 Access-Control-Allow-Origin: *,除非你明確知道這對你的應用程式是安全的。
  • 框架/函式庫: 如果你使用 PHP 框架(如 Laravel, Symfony, CodeIgniter),它們通常會提供內建的 CORS 配置選項或 Middleware (中介層) 來更方便、更安全地處理 CORS,你應該優先使用這些功能而不是手動設置標頭。例如,Laravel 有 fruitcake/laravel-cors 套件,可以透過配置文件輕鬆管理 CORS 策略。
  • 配置 Apache/Nginx: 另一種處理 CORS 的方式是在 Web 伺服器層級(Apache 或 Nginx)配置 CORS 標頭,而不是在每個 PHP 腳本中。這可以讓你的 PHP 程式碼更乾淨,並且在某些情況下可以提高效能。
    • Apache (在 .htaccess 或 httpd.conf 中):
      Apache
      <IfModule mod_headers.c>
          Header set Access-Control-Allow-Origin "http://localhost:3000"
          Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
          Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
          Header set Access-Control-Allow-Credentials "true"
          Header set Access-Control-Max-Age "86400"
      </IfModule>
      
    • Nginx (在 server 或 location 區塊中):
      Nginx
      add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With';
      add_header 'Access-Control-Allow-Credentials' 'true';
      add_header 'Access-Control-Max-Age' '86400';
      
      if ($request_method = 'OPTIONS') {
          add_header 'Content-Type' 'text/plain; charset=utf-8';
          add_header 'Content-Length' '0';
          return 204;
      }
      
      這種方式通常更推薦用於靜態資源或不需要複雜邏輯判斷的 API。
  • 安全考慮: CORS 本身並不是安全措施,它只是放鬆了瀏覽器的同源策略。它並不能保護你的伺服器免受未經授權的訪問。你仍然需要實施其他安全措施,例如認證、授權、輸入驗證、防止 SQL Injection 和 XSS 等。

總之,理解 CORS 的工作原理和如何在 PHP 中正確設置相關 HTTP 標頭,對於構建現代 Web 應用程式至關重要。希望這份詳細的說明對您有所幫助!

沒有留言:

張貼留言

網誌存檔