fetch 引發 blocked by CORS policy

fetch 引發 blocked by CORS policy

起步

當使用 fetch 函數做跨域請求時,大概率會在瀏覽器 Console 中看到這樣一個錯誤信息:Access to fetch at 'xxx' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

會出現上述錯誤是因爲你在做跨域請求,這是一項瀏覽器認爲不安全的操作。那麼如何鑑定是不是在跨域呢?要看 url 是否同源,同源的標準是:協議,域名,端口均相同。詳情可參看 瀏覽器的同源策略

除大公司外,我斗膽猜測一下你之所以需要 CORS (跨域資源共享),可能是以下原因之一:1. 你在寫 demo; 2. 你處於前後端分離開發。

接下來我會後端採用 flask 講述如何解決 CORS 被禁問題。選擇 flask 是因爲該框架在處理 Content-Type 爲 text/plainapplication/json 時存在明顯區別,有助實驗觀察。如果你使用的是其他語言,或者其他框架,都沒關係,原理是相通的。

從一個 demo 入手

假設當前我知道了 fetch 函數如何發起 post 請求,但我想測試一下,驗證我知道的對不對。於是我寫了下面這段 js 代碼:

fetch("http://127.0.0.1:8080/login", {
  method: "POST",
  body: JSON.stringify({
    username: "zhong",
    password: "zzZhong",
  }),
})
.then(resp => resp.json())
.then(data => console.log(data))

這段 js 的含義是,向 http://127.0.0.1:8080/login 發起 post 請求,並等待後端響應,最後將後端響應的數據打印到 Console 中。

畢竟我只是想簡單測試一下,偷懶起見,把 js 代碼嵌入 html 文件中,並且在瀏覽器裏以絕對路徑的方式直接訪問 html 文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
</body>
<script>

// js 代碼

</script>
</html>

在這裏插入圖片描述
後端暫時啥也別做,只要一接收到前端的請求,立馬返回:{“hello”: “world”}。後端代碼如下所示:

from flask import Flask

app = Flask(__name__)

@app.route("/login", methods=["POST"])
def index():
    return {"hello": "world"}

def main():
    app.debug = True
    app.run(host="0.0.0.0", port=8080)

if __name__ == "__main__":
    main()

照理說,只要我瀏覽器一回車,Console 中就會打印出 {“hello”: “world”}。但事實並非如此。這一回車,Console 中就出現篇頭提到的那個錯誤,說明你在跨域訪問了。可是爲甚麼會這樣呢?打開瀏覽器的 Network 探個明白。
在這裏插入圖片描述
當我在瀏覽器中輸入文件的絕對路徑後回車,瀏覽器使用的 file 協議訪問 html 文件。而 html 文件中的 js 代碼 (fetch) 發起的是 http 請求,即使用的是 http 協議。二者協議不同,所以非同源,瀏覽器出於安全考慮把請求禁了。

其實解決方法很簡單,只要在後端的響應頭中加上 Access-Control-Allow-Origin: * 即可。現在用 Python 把這個需求翻譯一下:

@app.route("/login", methods=["POST"])
def index():
    resp = Response()
    # 添加響應頭: Access-Control-Allow-Origin: *
    resp.headers["Access-Control-Allow-Origin"] = "*"
    # 字段對象轉 json 格式的字符串
    resp.data = json.dumps({"hello": "world"})
    return resp

此時在瀏覽器中刷新頁面,可以看到 Console 不會報錯,並且打印了後端返回的數據。查看後端返回的響應頭,可以看到我們確實把 Access-Control-Allow-Origin 加進去了。
在這裏插入圖片描述

後端如何解析參數

我們要做登陸功能,就需要解析前端提交的數據:用戶名密碼。對 flask 框架來說,request 對象有一個 json 屬性,而前端傳遞的數據是 json 字符串……感覺可以喲!

from flask import Flask, request, Response

...

@app.route("/login", methods=["POST"])
def index():
    resp = Response()
    resp.headers["Access-Control-Allow-Origin"] = "*"
    print(request.json)  # 注意打印內容
    return resp
...

很遺憾,程序跑起來終端打印 None。當 flask 框架不能解析到 json 數據時,request.json 就會爲 None。

再回到 Network,觀察 fetch 發起請求的請求頭。其中 Content-Type 爲 text/plain,也就是說,數據是以文本格式 (text/plain) 提交的,不是 json 格式,所以 flask 框架沒有解析到。post 提交數據格式可見 四種常見的 POST 提交數據方式
在這裏插入圖片描述
對 flask 來說,當提交數據格式爲 text/plain 時,後端正確解析數據方式:

@app.route("/login", methods=["POST"])
def index():    
    resp = Response()
    resp.headers["Access-Control-Allow-Origin"] = "*"
    # 讀取 http body
    content = request.input_stream.read(request.content_length)
    # byte -> utf-8
    resp.data = content.decode("utf-8")
    return resp

此時刷新瀏覽器,Console 裏就能看到 {username: “zhong”, password: “zzZhong”},說明我們成功拿到了前端提交的數據。

流行的 json 風格

當前互聯網流行傳遞 json 風格數據,fetch 也允許你這樣做,只要你在請求頭中加上 Content-Type: application/json 就好。js 代碼修改如下:

fetch("http://127.0.0.1:8080/login", {
  method: "POST",
  body: JSON.stringify({
    username: "zhong",
    password: "zzZhong",
  }),
  // 添加請求頭:application/json
  headers: {
    "Content-Type": "application/json"
  }
})
.then(resp => resp.json())
.then(data => console.log(data))

感覺上 request.json 不會是 None 了,那我們改後端代碼試一試。

@app.route("/login", methods=["POST"])
def index():
    resp = Response()
    resp.headers["Access-Control-Allow-Origin"] = "*"
    # dict -> json string
    resp.data = json.dumps(request.json)
    return resp

瀏覽器頁面刷起來,結果你會發現禁止 CORS 又出現了。其實這是瀏覽器的 preflight request 機制引起的。瀏覽器會自主發起一個預請求 (Content-Type 爲 text/plain 則沒有),請求方式爲 OPTIONS。瀏覽器倒沒別的意思,它就是想問問服務器:你允許我跨域請求嗎?服務器要是允許了,瀏覽器纔會發起 js 代碼中的 post 請求。

遇到這種情況禁止 CORS 是令人頭疼的,因爲瀏覽器發起的這個 options 請求沒有出現在 Network 中,而後端代碼又不允許該路由接收 OPTIONS 請求。“好事“都趕上了,所以一頭霧水。

正確後端代碼如下所示,刷新頁面也不會看到報錯信息了。

# 允許本路由被 POST, OPTIONS 請求
@app.route("/login", methods=["POST", "OPTIONS"])
def index():
    resp = Response()

    # 處理 post 請求
    if request.method == "POST":
        resp.headers["Access-Control-Allow-Origin"] = "*"
        resp.data = json.dumps(request.json)
    # 處理 options 請求
    elif request.method == "OPTIONS":
        # 設置響應頭
        resp.headers["Access-Control-Allow-Origin"] = "*"
        resp.headers["Access-Control-Allow-Headers"] = "*"
    
    return resp

服務器回覆瀏覽器要用“暗號”,暗號就擺在響應頭裏。

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
  • 第一行表示允許任何域跨於請求。
  • 第二行表示允許跨於請求的請求頭帶上任何字段。

其實這樣的允許尺度比較寬鬆,可能會爲生產環境帶來安全風險。如何最恰當配置響應頭可先閱讀 HTTP 響應首部字段 準確理解每個字段的含義,再着手設計。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章