AMP

AMP 中的 CORS

許多 AMP 元件和擴充功能利用跨來源資源共享 (CORS) 請求來使用遠端端點。本文件說明在 AMP 中使用 CORS 的主要面向。若要進一步瞭解 CORS 本身,請參閱 W3 CORS 規格

- [為什麼我自己的網域也需要 CORS?](#why-do-i-need-cors-for-my-own-origin-) - [在 CORS 請求中利用 Cookie](#utilizing-cookies-for-cors-requests) - [AMP 中的 CORS 安全性](#cors-security-in-amp) - [驗證 CORS 請求](#verify-cors-requests) - [1) 允許來自特定 CORS 網域的請求](#1-allow-requests-for-specific-cors-origins) - [2) 允許同網域請求](#2-allow-same-origin-requests) * [傳送 CORS 回應標頭](#send-cors-response-headers) - [Access-Control-Allow-Origin: <網域>](#access-control-allow-origin-origin) * [處理狀態變更請求](#processing-state-changing-requests) - [範例逐步解說:處理 CORS 請求與回應](#example-walkthrough-handing-cors-requests-and-responses) - [在 AMP 中測試 CORS](#testing-cors-in-amp)

為什麼我自己的網域也需要 CORS?

您可能會感到困惑,不明白為什麼您自己的網域請求也需要 CORS,讓我們深入探討一下。

擷取動態資料的 AMP 元件 (例如 amp-form、amp-list 等) 會向遠端端點發出 CORS 請求,以擷取資料。如果您的 AMP 頁面包含這類元件,您就需要處理 CORS,以避免這些請求失敗。

讓我們用一個範例來說明

假設您有一個 AMP 頁面,其中列出附帶價格的產品。若要更新頁面上的價格,使用者可以按一下按鈕,按鈕會從 JSON 端點擷取最新價格 (透過 amp-list 元件完成)。JSON 位於您的網域中。

好的,所以頁面在我的網域,而 JSON 也在我的網域。我看不到任何問題!

啊,但是您的使用者是如何連上您的 AMP 頁面的?他們存取的是快取頁面嗎?您的使用者很可能不是直接存取您的 AMP 頁面,而是透過其他平台找到您的頁面。例如,Google 搜尋使用 Google AMP 快取來快速呈現 AMP 頁面;這些是從 Google AMP 快取 (不同的網域) 提供的快取頁面。當您的使用者按一下按鈕來更新頁面上的價格時,快取的 AMP 頁面會向您的原始網域傳送請求以取得價格,這就是網域之間的不符 (快取 -> 原始網域)。若要允許這類跨網域請求,您需要處理 CORS,否則請求就會失敗。


好的,那我該怎麼做?

  1. 對於擷取動態資料的 AMP 頁面,請務必測試這些頁面的快取版本;不要只在您自己的網域上測試。(請參閱下方的「在 AMP 中測試 CORS」章節)
  2. 請按照本文件中的指示操作,以處理 CORS 請求和回應。

在 CORS 請求中利用 Cookie

大多數使用 CORS 請求的 AMP 元件都會自動設定 憑證模式,或者允許作者選擇性地啟用此模式。例如,amp-list 元件會從 CORS JSON 端點擷取動態內容,並允許作者透過 credentials 屬性設定憑證模式。

範例:透過 Cookie 在 amp-list 中加入個人化內容

<amp-list
  credentials="include"
  src="<%host%>/json/product.json?clientId=CLIENT_ID(myCookieId)"
>
  <template type="amp-mustache">
    Your personal offer: ${{price}}  </template>
</amp-list>

透過指定憑證模式,原始網域可以在 CORS 請求中加入 Cookie,也可以在回應中設定 Cookie (但須遵守第三方 Cookie 限制)。

瀏覽器中指定的相同第三方 Cookie 限制也適用於 AMP 中經過憑證驗證的 CORS 請求。這些限制取決於瀏覽器和平台,但在某些瀏覽器中,原始網域只有在使用者先前在第一方 (頂層) 視窗中造訪過該原始網域時,才能設定 Cookie。或者換句話說,只有在使用者直接造訪過原始網域網站本身之後才能設定 Cookie。鑑於此,透過 CORS 存取的服務不能假設它預設就能設定 Cookie。

AMP 中的 CORS 安全性

為了確保您的 AMP 頁面請求和回應有效且安全,您必須

  1. 驗證請求.
  2. 傳送適當的回應標頭.

如果您在後端使用 Node,則可以使用 AMP CORS 中介軟體,它是 AMP Toolbox 的一部分。

驗證 CORS 請求

當您的端點收到 CORS 請求時

  1. 驗證 CORS Origin 標頭是否為允許的網域 (發布商的網域 + AMP 快取).
  2. 如果沒有 Origin 標頭,請檢查請求是否來自同網域 (透過 AMP-Same-Origin).

1) 允許來自特定 CORS 網域的請求

CORS 端點會透過 Origin HTTP 標頭接收請求網域。端點應僅允許來自下列網域的請求:(1) 發布商自己的網域;以及 (2) https://cdn.ampproject.org/caches.json 中列出的每個 cacheDomain 網域。

例如,端點應允許來自下列網域的請求

  • Google AMP 快取子網域:https://<發布商網域>.cdn.ampproject.org
    (例如,https://nytimes-com.cdn.ampproject.org)

如需 AMP 快取網址格式的相關資訊,請參閱下列資源

2) 允許同網域請求

對於缺少 Origin 標頭的同網域請求,AMP 會設定下列自訂標頭

AMP-Same-Origin: true

當在同網域 (也就是從非快取網址提供的文件) 上發出 XHR 請求時,AMP Runtime 會傳送此自訂標頭。允許包含 AMP-Same-Origin:true 標頭的請求。

傳送 CORS 回應標頭

驗證 CORS 請求後,產生的 HTTP 回應必須包含下列標頭

Access-Control-Allow-Origin: <網域>

此標頭是 W3 CORS 規格的要求,其中 origin 是指透過 CORS Origin 請求標頭允許的請求網域 (例如,"https://<發布商子網域>.cdn.ampproject.org")。

雖然 W3 CORS 規格允許在回應中傳回 * 值,但為了提高安全性,您應

  • 如果 Origin 標頭存在,請驗證並回應 Origin 標頭的值。

處理狀態變更請求

處理請求之前,請先執行這些驗證檢查。此驗證有助於防止 CSRF 攻擊,並避免處理不受信任來源的請求。

在處理可能會變更系統狀態的請求 (例如,使用者訂閱或取消訂閱郵寄清單) 之前,請檢查下列項目

是否已設定 Origin 標頭:

  1. 如果網域與下列其中一個值不符,請停止並傳回錯誤回應

    • <發布商網域>.cdn.ampproject.org
    • 發布商的網域 (也就是您的網域)

    其中 * 代表萬用字元比對,而不是實際的星號 (*)。

  2. 否則,請處理請求。

如果設定 Origin 標頭:

  1. 驗證請求是否包含 AMP-Same-Origin: true 標頭。如果請求未包含此標頭,請停止並傳回錯誤回應。
  2. 否則,請處理請求。

範例逐步解說:處理 CORS 請求與回應

在 CORS 請求傳送到您的端點時,有兩種情境需要考量

  1. 來自同網域的請求。
  2. 來自快取網域 (來自 AMP 快取) 的請求。

讓我們透過範例逐步解說這些情境。在我們的範例中,我們管理 example.com 網站,該網站託管名為 article-amp.html 的 AMP 頁面。AMP 頁面包含 amp-list,可從也託管在 example.com 上的 data.json 檔案擷取動態資料。我們想要處理來自 AMP 頁面且傳送到我們 data.json 檔案的請求。這些請求可能來自同網域 (非快取) 的 AMP 頁面,或來自不同網域 (快取) 的 AMP 頁面。


允許的網域

根據我們從上述「驗證 CORS 請求」中瞭解到的 CORS 和 AMP 相關資訊,針對我們的範例,我們將允許來自下列網域的請求

  • example.com --- 發布商的網域
  • example-com.cdn.ampproject.org --- Google AMP 快取子網域

允許請求的回應標頭

對於來自允許網域的請求,我們的回應將包含下列標頭

Access-Control-Allow-Origin: <origin>

這些是我們可能會加入 CORS 回應的其他回應標頭

Access-Control-Allow-Credentials: true
Content-Type: application/json
Access-Control-Max-Age: <delta-seconds>
Cache-Control: private, no-cache

虛擬 CORS 邏輯

我們用於處理 CORS 請求和回應的邏輯可以簡化為下列虛擬程式碼

IF CORS header present
   IF origin IN allowed-origins
      allow request & send response
   ELSE
      deny request
ELSE
   IF "AMP-Same-Origin: true"
      allow request & send response
   ELSE
      deny request

CORS 範例程式碼

以下是我們可以使用的範例 JavaScript 函式,以處理 CORS 請求和回應

function assertCors(req, res, opt_validMethods, opt_exposeHeaders) {
  var unauthorized = 'Unauthorized Request';
  var origin;
  var allowedOrigins = [
    'https://example.com',
    'https://example-com.cdn.ampproject.org',
    'https://cdn.ampproject.org',
  ];
  var allowedSourceOrigin = 'https://example.com'; //publisher's origin
  // If same origin
  if (req.headers['amp-same-origin'] == 'true') {
    origin = sourceOrigin;
    // If allowed CORS origin & allowed source origin
  } else if (
    allowedOrigins.indexOf(req.headers.origin) != -1 &&
    sourceOrigin == allowedSourceOrigin
  ) {
    origin = req.headers.origin;
  } else {
    res.statusCode = 403;
    res.end(JSON.stringify({message: unauthorized}));
    throw unauthorized;
  }

  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Access-Control-Allow-Origin', origin);
}

注意:如需實際運作的程式碼範例,請參閱 amp-cors.js

情境 1:來自同網域 AMP 頁面的 Get 請求

在下列情境中,article-amp.html 頁面請求 data.json 檔案;網域相同。


如果我們檢查請求,我們會發現

Request URL: https://example.com/data.json
Request Method: GET
AMP-Same-Origin: true

由於此請求來自同網域,因此沒有 Origin 標頭,但存在 AMP-Same-Origin: true 的自訂 AMP 請求標頭。由於此請求來自同網域 (https://example.com),我們可以允許此請求。

我們的回應標頭會是

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com

情境 2:來自快取 AMP 頁面的 Get 請求

在下列情境中,Google AMP 快取上快取的 article-amp.html 頁面請求 data.json 檔案;網域不同。


如果我們檢查此請求,我們會發現

Request URL: https://example.com/data.json
Request Method: GET
Origin: https://example-com.cdn.ampproject.org

由於此請求包含 Origin 標頭,我們將驗證它是否來自允許的網域。由於此請求來自允許的網域,我們可以允許此請求。

我們的回應標頭會是

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example-com.cdn.ampproject.org

使用快取字型

Google AMP 快取會快取 AMP HTML 文件、圖片和字型,以最佳化 AMP 頁面的速度。在加快 AMP 頁面速度的同時,我們也希望謹慎保護快取資源的安全。我們將變更 AMP 快取回應其快取資源 (通常是字型) 的方式,方法是遵循原始網域的 Access-Control-Allow-Origin 值。

過去的行為 (2019 年 10 月之前)

當 AMP 頁面從 @font-face src 屬性載入 https://example.com/some/font.ttf 時,AMP 快取會快取字型檔案,並以下列方式提供資源,並具有萬用字元 Access-Control-Allow-Origin

  • 網址 https://example-com.cdn.ampproject.org/r/s/example.com/some/font.tff
  • Access-Control-Allow-Origin: *

新的行為 (2019 年 10 月及之後)

雖然目前的實作方式較為寬鬆,但這可能會導致跨網域網站意外使用字型。在此變更中,AMP 快取將開始回應與原始伺服器回應的完全相同的 Access-Control-Allow-Origin 值。若要從快取的 AMP 文件正確載入字型,您需要透過標頭接受 AMP 快取網域。

範例實作方式會是

function assertFontCors(req, res, opt_validMethods, opt_exposeHeaders) {
  var unauthorized = 'Unauthorized Request';
  var allowedOrigins = [
    'https://example.com',
    'https://example-com.cdn.ampproject.org',
  ];
  // If allowed CORS origin
  if (allowedOrigins.indexOf(req.headers.origin) != -1) {
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
  } else {
    res.statusCode = 403;
    res.end(JSON.stringify({message: unauthorized}));
    throw unauthorized;
  }
}

例如,如果您想要在 https://example.com/amp.html 中載入 /some/font.ttf,則原始伺服器應回應 Access-Control-Allow-Origin 標頭,如下所示。


如果您的字型檔案可以從任何網域存取,您可以回應萬用字元 Access-Control-Allow-Origin,AMP 快取也會回應該值,表示它會回應 Access-Control-Allow-Origin: *。如果您已進行此設定,則無需進行任何變更。

我們計畫在 2019 年 10 月中旬左右進行此變更,並預期每個使用自架託管字型的 AMP 發布商都會檢查是否受到影響。

推出計畫

  • 2019-09-30:發布版本包含更精確的控制,可控制此變更適用的網域。此建置版本應在本週內推出。
  • 2019-10-07:測試網域將啟用以進行手動測試。
  • 2019-10-14:(但取決於測試進度):此功能將全面推出。

請追蹤相關的問題追蹤記錄

在 AMP 中測試 CORS

當您測試 AMP 頁面時,請務必納入來自 AMP 頁面快取版本的測試。

透過快取網址驗證頁面

為了確保您的快取 AMP 頁面能正確呈現和運作

  1. 從您的瀏覽器開啟 AMP 快取將用於存取您的 AMP 頁面的網址。您可以從 AMP By Example 上的此工具判斷快取網址格式。

    例如

    • 網址:https://amp.dev.org.tw/documentation/guides-and-tutorials/start/create/
    • AMP 快取網址格式:https://www-ampproject-org.cdn.ampproject.org/c/s/www.ampproject.org/docs/tutorials/create.html
  2. 開啟瀏覽器的開發人員工具,並驗證沒有錯誤,且所有資源都已正確載入。

驗證您的伺服器回應標頭

您可以使用 curl 命令來驗證您的伺服器是否傳送正確的 HTTP 回應標頭。在 curl 命令中,提供請求網址和您想要加入的任何自訂標頭。

語法curl <請求網址> -H <自訂標頭> - I

測試來自同網域的請求

在同網域請求中,AMP 系統會加入自訂 AMP-Same-Origin:true 標頭。

以下是我們的 curl 命令,用於測試從 https://amp.dev.org.twexamples.json 檔案 (在同網域上) 的請求

curl 'https://amp.dev.org.tw/static/samples/json/examples.json' -H 'AMP-Same-Origin: true' -I

命令的結果會顯示正確的回應標頭 (注意:額外資訊已修剪)

HTTP/2 200
access-control-allow-headers: Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token
access-control-allow-credentials: true
access-control-allow-origin: https://amp.dev.org.tw
access-control-allow-methods: POST, GET, OPTIONS

測試來自快取 AMP 頁面的請求

在非來自同網域 (也就是快取) 的 CORS 請求中,origin 標頭是請求的一部分。

以下是我們的 curl 命令,用於測試從 Google AMP 快取上的快取 AMP 頁面到 examples.json 檔案的請求

curl 'https://amp.dev.org.tw/static/samples/json/examples.json' -H 'origin: https://ampbyexample-com.cdn.ampproject.org' -I

命令的結果會顯示正確的回應標頭

HTTP/2 200
access-control-allow-headers: Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token
access-control-allow-credentials: true
access-control-allow-origin: https://ampbyexample-com.cdn.ampproject.org
access-control-allow-methods: POST, GET, OPTIONS