CORS
跨來源資料共用 CORS (Cross-Origin Resource Sharing) 與同源政策 (Same-origin policy),是前端開發時常會踩到的坑。當瀏覽器對不同來源發出請求時會建立一個跨域請求(cross-origin HTTP request),例如在 http://domain-a.com HTML 使用一個 src 為 http://domain-b.com/image.jpg 的 img tag,這樣即為一個跨域請求。在一些條件下跨域請求會被瀏覽器阻擋,而無法取得 Response。
基於安全考量,瀏覽器發出的 Request 都要遵守同源政策 (Same-origin policy)。除非服務端使用 CORS 標頭,否則瀏覽器只能請求同源的 HTTP 資源。要特別注意的是網域(domain)、通訊協定(protocol)或通訊埠(port)只要有任一個不同都視為不同源,如 localhost:8080 與 localhost:8000 一樣被視為不同源。
跨域請求一般而言分為簡單請求與預檢請求兩種:
-
簡單請求 Simple requests:
一般而言滿足以下條件的 Request 為簡單請求- HTTP Methods:
- GET
- HEAD
- POST
- Content-Type:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
除了 Method 有限定外,Content-Type 其實也有限定,所以一般 RESTful 常用的 POST 搭配 application/json 也會被同源政策阻擋。
若為瀏覽器會直接發出 Request,並取得正常的回應,如下圖:
Image Source: MDN web docs 2. 預檢請求 Preflighted requests:
預檢請求分為兩個步驟執行,當瀏覽器發現該 Request 是預檢請求時會先發出一個 OPTIONS Request 並告知實際要發出的 Method 與相關資訊,服務端再根據這些資訊回覆是否可以執行該 Request,瀏覽器確認服務端允許後才會發出真正的 Request,如下圖:
Image Source: MDN web docs
如果被阻擋就通常會得到No 'Access-Control-Allow-Origin' header is present on the requested resource.
或Cross-Origin Request Blocked
這類的錯誤訊息。
CORS 常見的前端 Workaround 通常是使用 JSONP ,但隨著網路服務模式的改變,現在許多服務多以 API 的形式提供,所以也都會有搭配正常的 CORS 讓使用者使用,那些 Workaround 也逐漸沒什麼人在使用了。
而除了 Request 外,網頁的元素操作很多地方都會踩到 Same-origin policy 的坑,如不同網域之間的 iframe 操作 SecurityError: Blocked a frame with origin from accessing a cross-origin frame,其核心概念就是如果不同源,有很多事情瀏覽器都會基於安全考量都擋掉。
到這邊可以發現 CORS 與 Same-origin policy 都是瀏覽器負責檢核,若單純指使從後端發 Request 完全不用考慮這些問題,因此也有很多 Workaround 是在後端作一個跳板間接取得資源,或直接用 cors-anywhere 這類第三方服務當跳板。
CORS 其實還有很多詳細內容,但這邊跳過了一些細節僅簡單說明原理,詳細可以參考 MDN web docs - 跨來源資源共用(CORS),十分值得一讀。
Backend CORS 設定
前端是否能執行跨域請求完全是依賴於後端是否有設定 CORS,Enable CORS 列出了一些常見的後端 Server 設定方法。以下針對 Python 常用的 Django 與 Flask 後端框架說明設定方法。
Django
在使用 Django REST framework 做為網站後端的時,前端頁面通常會是不同來源,可以使用 django-cors-headers 解決 CORS 的問題。
# settings.py
INSTALLED_APPS = (
...
'corsheaders',
...
)
MIDDLEWARE_CLASSES = (
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
)
CORS_ORIGIN_ALLOW_ALL = True # 允許所有的跨站請求, 白名單不會被啟用
# 白名單設定
CORS_ORIGIN_WHITELIST = (
'http://localhost:9000',
'http://example.com',
...
)
Flask
以 Flask 作為後端時,可以使用 Flask-CORS 解決 CORS 的問題。
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
@app.route("/")
def helloWorld():
return "Hello, cross-origin-world!"
Reference:
- MDN web docs - 跨來源資源共用(CORS)
- MDN web docs - 同源政策 (Same-origin policy)
- 深入認識跨域請求
- 更安全的請求標頭 - Fetch Metadata Request Headers