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/plain 和 application/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 響應首部字段 準確理解每個字段的含義,再着手設計。