起步
当使用 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 响应首部字段 准确理解每个字段的含义,再着手设计。
还不快抢沙发