存取控制系統是執行授權識別、認證、存取核可的實體,也負責透過登入來進行驗證,包含密碼、個人身份識別碼(personal identification numbers,PINs)、生物辨識掃描,以及物理或電子的金鑰。
存取控制是用於規範計算環境之資源可供哪些人或單位觀看、使用的一種安全技術。
瀏覽器透過 XMLHttpRequest
或 Fetch 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
GET
或POST
方法送出之請求。若為 POST,則該請求之Content-Type
標頭值須為application/x-www-form-urlencoded
、multipart/form-data
或text/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
標頭值。這個範例可以在這裡看到執行的情形。
預檢請求
預檢存取控制請求在以下情況下發起:
- 使用一個
GET
或POST
以外的 HTTP 方法。或是在使用POST
方法的情況下,其Content-Type
標頭值為application/x-www-form-urlencoded
、multipart/form-data
或text/plain
以外的值。例如,假設使用POST
方法並包含之Content-Type
標頭值為application/xml
,則此請求便為預檢(preflighted)請求。 - 一個帶有自定義 HTTP 標頭(如
X-PINGARUNER
)的請求。
在 HTTP 存取控制(CORS)一文的預檢存取控制請求章節示範了客戶端與伺服器端之間標頭的交流。一個伺服器資源要回應預檢(preflighted)請求必須能夠進行以下的判斷:
- 基於
Origin
的篩選,如果有的話。 - 回應一個
OPTIONS
請求(即預檢(preflighted)請求),包含寄送必要的Access-Control-Allow-Methods
、Access-Control-Allow-Headers
標頭值(假如有任何應用程式運作所需要的額外標頭),以及若是此資源要求身分驗證,則需要包含Access-Control-Allow-Credentials
標頭。 - 回應實際(actual)請求,包含處理
POST
請求的資料等等。
下面是一個使用 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