作者:Chancel, 更新:2019 Dec 02, 字数:6610, 已阅:600
处理跨域问题的时候都是通过Google来解决,但从未深入的了解一下是什么原因造成的跨域问题,这次要好好了解到底是什么引起跨域请求失败以及通用的解决方案,请耐心看完文章,遇到不懂的名词也没关系,看完之后再一一了解相关名词也来得及,相信看完之后你会对跨域请求有一个细致的了解。
当一个请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。 现在常见于Ajax技术动态发起的跨域请求,如www.a.com下发起一个www.b.com的请求即被视为是跨域请求,当无任何设置的时候,跨域请求会被拒绝,原因就是同源策略
如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。 同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。 简言之,浏览器的同源策略会限制两个不同来源的资源进行交互(不同域名、端口),这是一个用于隔离潜在恶意文件的重要安全机制,最主要的便是防止CSRF攻击
因为CSRF攻击的存在,出于安全问题的考虑,大部分浏览器的默认安全限制为同源策略,即JavaScript或Cookie只能访问当前域名的内容
但安全也是相对的,如果在银行A网站无法访问B网站,那A网站上如果需要显示个人信用分的C网站的一些信息将变得不可实现
总不能A网站实时抓取C网站的数据吗?那么A网站要如何“正确的”访问B网站呢?答案是跨源资源共享
Cross-Origin Resource Sharing 跨源资源共享,是W3C组织推荐的一种跨域访问机制,这种机制旨在让跨站数据传输更加安全,减轻跨域HTTP请求的风险。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 ——阮一峰
基于上述所说的CSRF攻击的风险,现各主流的浏览器都会对动态(Ajax)的跨域请求进行特殊的验证处理。
这种特殊的验证处理分为两种,分别是预先请求验证处理和简单请求验证处理两种方式,他们之间的区别如下
当代码请求符合以下两个标准时,则为简单请求
简单请求时,浏览器会直接发送跨域请求,并在请求头中携带Origin字段以表明这是一个跨域的请求,服务器端接到请求后,根据自己的跨域规则设置Access-Control-Allow-Origin和Access-Control-Allow-Methods的响应头,来返回验证结果,两者信息一致,浏览器则接收返回的资源内容。
预先请求验证处理,可以理解为起飞前的预飞,当请求方式不合乎简单请求标准时,浏览器都会先发送一次预先请求验证处理,以确定服务器能够支持特殊(不合乎标准)的跨域请求
然而,当前大部分主流网页的Content-Type都是application/json,当请求头为application/json时,请求就不是标准的简单请求,此时会触发预先请求验证处理,即在Get/Post请求发生前,会有一次OPTIONS请求的动作(可以在控制台的Network中看到),这就是Preflighted requests(预先验证请求)
客户端请求:重点是此次OPTIONS里Origin的值,这个值将向服务器表达自己的来源,服务器通常也会限定指定域名的访问(如信用分C网站允许A网站的跨域请求),同时也表明了这是一个跨域请求
对于OPTIONS请求,我们关注这三个请求头,这三个请求头部到达服务端后,服务端必须设置这三个请求头部作为response的header返回给浏览器 浏览器根据Response中的这三个请求头部来判断服务端是否允许此次请求
当跨域请求失败时,通常可以通过浏览器console(控制台)查看具体验证失败原因,此时要结合控制台的Network判断具体失败请求的请求方式是简单请求还是预先验证请求。 根据上文了解跨域请求的原理之后,解决方案就非常清晰了,根据不同的业务场景来做相对应的处理方式,这里举两个例子,以Nginx和Python后端分别来处理跨域请求做示例。
一次完整的非简单请求跨域请求的过程如下 客户端发起跨域请求,携带自定义头secret,此时非标准请求,触发预先验证请求,在控制台中看到发生了一次OPTIONS请求
General
Request URL: http://127.0.0.1:37364/
Request Method: OPTIONS # OPTIONS方法
Status Code: 200 OK
Remote Address: 127.0.0.1:37364
Referrer Policy: no-referrer-when-downgrade
Response Headers
Access-Control-Allow-Headers: secret # 服务器允许跨域自定义头secret
Access-Control-Allow-Methods: POST # 服务器允许跨域的请求方法
Access-Control-Allow-Origin: * # 服务器允许跨域的来源
Date: Mon, 02 Dec 2019 09:46:41 GMT
Server: Microsoft-HTTPAPI/2.0
Transfer-Encoding: chunked
Request Headers
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,zh-HK;q=0.8
Access-Control-Request-Headers: secret # 客户端自定义的secret头
Access-Control-Request-Method: POST # 客户端所使用的POST请求方法
Cache-Control: no-cache
Connection: keep-alive
Host: 127.0.0.1:37364
Origin: null # 客户端的来源
Pragma: no-cache
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
可以看到客户端的请求符合服务端对跨域的要求,此时预先验证请求成功通过,浏览器随即发起原本的POST请求,请求信息如下
General
Request URL: http://127.0.0.1:37364/
Request Method: POST
Status Code: 200 OK
Remote Address: 127.0.0.1:37364
Referrer Policy: no-referrer-when-downgrade
Response Headers
Access-Control-Allow-Origin: * # 注意,即便预先验证通过,跨域请求仍然需要服务器返回允许跨域的来源,否则依旧无法正常接收数据
Date: Mon, 02 Dec 2019 09:46:41 GMT
Server: Microsoft-HTTPAPI/2.0
Transfer-Encoding: chunked
Request Headers
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,zh-HK;q=0.8
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 6
Content-type: text/plain
Host: 127.0.0.1:37364
Origin: null
Pragma: no-cache
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
secret: tyerp
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
方案一:让Nginx允许跨域访问
location / {
# 预先请求,需要告诉浏览器服务器允许跨域请求以及跨域请求的一些细节(如方法、请求头等)
if ($request_method = 'OPTIONS') {
# *号表示允许从任何域名发起的请求,根据业务需要可修改为指定域名
add_header 'Access-Control-Allow-Origin' '*';
# 表示支持的跨域请求方法
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# 表示此次预检有效期为20天
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*';
proxy_pass http://127.0.0.1:5000;
方案二:后端程序CROS的处理方式,以Python的Flask框架为例子
# 在请求正式进入流程前
@app.before_request
def option_replay():
# 判断到OPTIONS请求,修改Response的头信息,允许跨域
if request.method ==‘OPTIONS‘:
resp = app.make_default_options_response()
if ‘ACCESS_CONTROL_REQUEST_HEADERS‘ in request.headers:
resp.headers[‘Access-Control-Allow-Headers‘] = request.headers[‘ACCESS_CONTROL_REQUEST_HEADERS‘]
resp.headers[‘Access-Control-Allow-Methods‘] = request.headers[‘Access-Control-Request-Method‘]
resp.headers[‘Access-Control-Allow-Origin‘] = request.headers[‘Origin‘]
return resp
# 在请求正式进入流程后
@app.after_request
def set_allow_origin(resp):
h = resp.headers
# 判断非OPTIONS请求,添加Access-Control-Allow-Origin允许跨域访问
if request.method != ‘OPTIONS‘ and ‘Origin‘ in request.headers:
h[‘Access-Control-Allow-Origin‘] = request.headers[‘Origin‘]
可以看到,核心思想都是一致的,都是修改预先验证请求的规则来达到允许跨域请求的效果。