伺服器端存取控制(CORS)

瀏覽器透過 XMLHttpRequestFetch API 所發起的跨網域請求(cross-site request),會在寄送時使用特定的 HTTP 標頭。同樣的,由伺服器回傳的跨網域回應(cross-site response)中也能看到特定的 HTTP 標頭。關於這些特定標頭的簡介,包括使用 JavaScript 發起請求與處理來自伺服器回應的範例以及每一個標頭的討論,可以在 HTTP 存取控制(CORS)一文中找到,並應該搭配本文一起閱讀。這篇文章包含使用 PHP 處理存取控制請求與建立存取控制回應。本文的目標讀者為伺服器端程式設計師或管理員。僅管本篇範例使用的是 PHP,但類似的概念也適用於 ASP.net、Perl、Python、Java 等等其他語言;一般來說,這些概念也能套用在任何處理 HTTP 請求及動態建立 HTTP 回應的伺服器端程式環境中。

HTTP 標頭討論

討論到同時涵蓋客戶端及伺服器端使用的 HTTP 標頭的文章在此,建議先閱讀該篇文章。

可執行的程式碼範例

隨後章節的 PHP 程式碼片段(以及 JavaScript 呼叫伺服器)可以在這裡取得。這些程式在實作了跨網域 XMLHttpRequest 的瀏覽器中都可以運作。

簡單跨網域請求

簡單存取控制請求會在以下情況下被建立發起:

  • 一個 HTTP/1.1 GETPOST 方法送出之請求。若為 POST,則該請求之 Content-Type 標頭值須為 application/x-www-form-urlencodedmultipart/form-datatext/plain 其中之一。
  • HTTP 請求中沒有使用自定義的標頭(如 X-Modified 等等)。

在此情況下,回傳回應需要考慮以下條件:

  • 如果該資源是允許被任何人存取的(就像所有透過 GET 方法存取的 HTTP 資源),則只要回傳帶有 Access-Control-Allow-Origin: * 標頭值的回應即可。除非資源需要身分憑證(credentials),如 Cookies 與 HTTP 驗證(Authentication)資訊。
  • 如果資源應該要限制請求者的網域(domain),或是假如資源需要身分憑證(credentials)來進行存取(或是要設定憑證)。則篩選請求的 Origin 標頭就可能是必要的,或至少呼應請求者的 Origin 標頭值(例如 Access-Control-Allow-Origin: http://arunranga.com)。另外,將會發送 Access-Control-Allow-Credentials: true 標頭,在下面的章節會進行討論。

在 HTTP 存取控制(CORS)一文的簡單存取控制請求章節示範了客戶端與伺服器端之間標頭的交流。下面是一個處理簡單請求的 PHP 程式碼片段:

<?php

// 我們僅授權讓 arunranga.com 網域來存取資源
// 因為我們認為該網域存取本 application/xml 資源是安全的

if($_SERVER['HTTP_ORIGIN'] == "http://arunranga.com") {
    header('Access-Control-Allow-Origin: http://arunranga.com');
    header('Content-type: application/xml');
    readfile('arunerDotNetResource.xml');
} else {    
  header('Content-Type: text/html');
  echo "<html>";
  echo "<head>";
  echo "   <title>Another Resource</title>";
  echo "</head>";
  echo "<body>",
       "<p>This resource behaves two-fold:";
  echo "<ul>",
         "<li>If accessed from <code>http://arunranga.com</code> it returns an XML document</li>";
  echo   "<li>If accessed from any other origin including from simply typing in the URL into the browser's address bar,";
  echo   "you get this HTML document</li>", 
       "</ul>",
     "</body>",
   "</html>";
}
?>

以上的程式會檢查由瀏覽器所送出之請求的 Origin 標頭(透過取得 $_SERVER['HTTP_ORIGIN'])是否為「http://arunranga.com」。若相符,則回傳之回應中加入 Access-Control-Allow-Origin: http://arunranga.com 標頭值。這個範例可以在這裡看到執行的情形

預檢請求

預檢存取控制請求在以下情況下發起:

  • 使用一個 GETPOST 以外的 HTTP 方法。或是在使用 POST 方法的情況下,其 Content-Type 標頭值為 application/x-www-form-urlencodedmultipart/form-datatext/plain 以外的值。例如,假設使用 POST 方法並包含之 Content-Type 標頭值為 application/xml,則此請求便為預檢(preflighted)請求。
  • 一個帶有自定義 HTTP 標頭(如 X-PINGARUNER)的請求。

在 HTTP 存取控制(CORS)一文的預檢存取控制請求章節示範了客戶端與伺服器端之間標頭的交流。一個伺服器資源要回應預檢(preflighted)請求必須能夠進行以下的判斷:

下面是一個使用 PHP 實作之處理預檢請求的範例:

<?php 

if($_SERVER['REQUEST_METHOD'] == "GET") {

  header('Content-Type: text/plain');
  echo "This HTTP resource is designed to handle POSTed XML input";
  echo "from arunranga.com and not be retrieved with GET"; 

} elseif($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
  // 告訴客戶端我們支援來自 arunranga.com 的呼叫
  // 以及這個預檢請求的有效期限僅有 20 天

  if($_SERVER['HTTP_ORIGIN'] == "http://arunranga.com") {
    header('Access-Control-Allow-Origin: http://arunranga.com');
    header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
    header('Access-Control-Allow-Headers: X-PINGARUNER');
    header('Access-Control-Max-Age: 1728000');
    header("Content-Length: 0");
    header("Content-Type: text/plain");
    //exit(0);
  } else {
    header("HTTP/1.1 403 Access Forbidden");
    header("Content-Type: text/plain");
    echo "You cannot repeat this request";
  }

} elseif($_SERVER['REQUEST_METHOD'] == "POST") {
  // 處理 POST 請求,第一步為取得 POST 請求中 blob 型態的 XML
  // 並且對其做一些處理,再傳送結果給客戶端
 
  if($_SERVER['HTTP_ORIGIN'] == "http://arunranga.com") {
    $postData = file_get_contents('php://input');
    $document = simplexml_load_string($postData);
    
    // 對 POST 請求的資料做一些處理

    $ping = $_SERVER['HTTP_X_PINGARUNER'];
         
    header('Access-Control-Allow-Origin: http://arunranga.com');
    header('Content-Type: text/plain');
    echo // 在處理之後要回傳的一些回應字串
  } else {
    die("POSTing Only Allowed from arunranga.com");
  }
} else {
    die("No Other Methods Allowed");
}
?>

注意範例中在回應 OPTIONS 預檢(preflighted)請求與回應 POST 請求時都會回傳相對應的 HTTP 標頭值,因此一個伺服器資源可以處理預檢以及實際(actual)請求。在回應 OPTIONS 預檢請求之回應標頭中,伺服器告知客戶端可以使用 POST 方法發送實際(actual)請求,並且能於實際(actual)請求的 HTTP 標頭欄位中帶上 X-PINGARUNER 及其值。這個範例可以在這裡看到執行的情形

身分憑證請求

身分憑證存取控制請求——即請求可以附帶 Cookies 或 HTTP 驗證(Authentication)訊息(並期望回應攜帶 Cookies)——可以是簡單預檢請求,根據請求使用之 HTTP 方法而定。

簡單請求情境中,請求將會連同 Cookies 一起發送(例如當 withCredentials 旗標被設置於 XMLHttpRequest 時)。假如伺服器以附帶了 Access-Control-Allow-Credentials: true 標頭值的身分憑證回應來回傳,則回應會被客戶端接受並且可被用於網頁內容中。在預檢請求中,伺服器可以用 Access-Control-Allow-Credentials: true 標頭來回應 OPTIONS 預檢請求。

以下為一些處理身分憑證請求的 PHP 程式片段:

<?php

if($_SERVER['REQUEST_METHOD'] == "GET") {
  header('Access-Control-Allow-Origin: http://arunranga.com');
  header('Access-Control-Allow-Credentials: true');
  header('Cache-Control: no-cache');
  header('Pragma: no-cache');
  header('Content-Type: text/plain');

  // 檢查有沒有 Cookie,若沒有則當作是第一次訪問
  if (!isset($_COOKIE["pageAccess"])) {
    setcookie("pageAccess", 1, time()+2592000);
    echo 'I do not know you or anyone like you so I am going to';
    echo 'mark you with a Cookie :-)';    
  } else {
    $accesses = $_COOKIE['pageAccess'];
    setcookie('pageAccess', ++$accesses, time()+2592000);
    echo 'Hello -- I know you or something a lot like you!';
    echo 'You have been to ', $_SERVER['SERVER_NAME'], ';
    echo 'at least ', $accesses-1, ' time(s) before!';
  }  
} elseif($_SERVER['REQUEST_METHOD'] == "OPTIONS") {
  // 告訴客戶端這個預檢請求的有效期限僅有 20 天
  if($_SERVER['HTTP_ORIGIN'] == "http://arunranga.com") {
    header('Access-Control-Allow-Origin: http://arunranga.com');
    header('Access-Control-Allow-Methods: GET, OPTIONS');
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Max-Age: 1728000');
    header("Content-Length: 0");
    header("Content-Type: text/plain");
  } else {
    header("HTTP/1.1 403 Access Forbidden");
    header("Content-Type: text/plain");
    echo "You cannot repeat this request";
  }
} else {
  die("This HTTP Resource can ONLY be accessed with GET or OPTIONS");
}
?>

注意此範例中的身分憑證請求,其中的 Access-Control-Allow-Origin: 標頭值不得是萬用字元(wildcard)「*」。此標頭值必須為一個有效的的來源網域(origin domain)。以上的範例可以在這裡看到執行的情形

Apache 範例

限制存取某些 URI

一個實用的訣竅是使用 Apache rewrite 環境變數(environment variable),並且讓 HTTP 標頭套用 Access-Control-Allow-* 至某些 URI。這相當有用,例如要限制跨來源(cross-origin)請求 GET /api(.*).json 為不帶身分憑證的請求:

RewriteRule ^/api(.*)\.json$ /api$1.json [CORS=True]
Header set Access-Control-Allow-Origin "*" env=CORS
Header set Access-Control-Allow-Methods "GET" env=CORS
Header set Access-Control-Allow-Credentials "false" env=CORS

參見

文件標籤與貢獻者

 此頁面的貢獻者: jackblackevo
 最近更新: jackblackevo,