menu Chancel's blog
rss_feed
Chancel's blog
我就是这样的人

跨域请求(CROS)的原理以及解决方案

作者:Chancel, 更新:2019 Dec 02, 字数:6610, 已阅:600

这篇文章更新于 1273 天前,文中部分信息可能失效,请自行甄别无效内容。

处理跨域问题的时候都是通过Google来解决,但从未深入的了解一下是什么原因造成的跨域问题,这次要好好了解到底是什么引起跨域请求失败以及通用的解决方案,请耐心看完文章,遇到不懂的名词也没关系,看完之后再一一了解相关名词也来得及,相信看完之后你会对跨域请求有一个细致的了解。

跨域

当一个请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。 现在常见于Ajax技术动态发起的跨域请求,如www.a.com下发起一个www.b.com的请求即被视为是跨域请求,当无任何设置的时候,跨域请求会被拒绝,原因就是同源策略

同源策略

如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。 同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。 简言之,浏览器的同源策略会限制两个不同来源的资源进行交互(不同域名、端口),这是一个用于隔离潜在恶意文件的重要安全机制,最主要的便是防止CSRF攻击

CSRF攻击 - Cross-site request forgery

  • CSRF攻击的步骤
    1. 用户访问银行网站A,通过用户身份验证成功登录A网站,A网站产生cookie信息并返回给用户的浏览器
    2. 用户A此时再访问网站B,网站B收到用户请求后,返回一个页面,页面中包含一个发送给网站A的转账请求
    3. 浏览器执行页面代码,发送网站B中的请求给网站A,因为浏览器与网站A已存在cookie,验证通过,网站A完成转账动作

因为CSRF攻击的存在,出于安全问题的考虑,大部分浏览器的默认安全限制为同源策略,即JavaScript或Cookie只能访问当前域名的内容

但安全也是相对的,如果在银行A网站无法访问B网站,那A网站上如果需要显示个人信用分的C网站的一些信息将变得不可实现

总不能A网站实时抓取C网站的数据吗?那么A网站要如何“正确的”访问B网站呢?答案是跨源资源共享

CROS - 跨源资源共享

Cross-Origin Resource Sharing 跨源资源共享,是W3C组织推荐的一种跨域访问机制,这种机制旨在让跨站数据传输更加安全,减轻跨域HTTP请求的风险。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 ——阮一峰

基于上述所说的CSRF攻击的风险,现各主流的浏览器都会对动态(Ajax)的跨域请求进行特殊的验证处理。

这种特殊的验证处理分为两种,分别是预先请求验证处理简单请求验证处理两种方式,他们之间的区别如下

  1. 如果你的请求非常标准,符合相对应的要求,则被浏览器视为简单请求,不会触发预检请求,一次请求解决跨域问题
  2. 如果你的请求带自定义头,自定义数据类型(如application/json)则会被浏览器视为非简单请求,在这次请求发出之前,浏览器得提前确定目标服务器是否接受这样的请求,浏览器会提前发出"OPTIONS"请求询问目标服务器是否支持跨域请求,根据服务器的回答决定是否发起此次请求。

简单请求与预先请求验证

简单请求

当代码请求符合以下两个标准时,则为简单请求

  • 请求方法限定
    1. GET
    2. POST
    3. HEAD
  • Headers限定
    1. Accept
    2. Accept-Language
    3. Content-Language
    4. Last-Event-ID
    5. Content-Type(application/x-www-form-urlencoded、multipart/form-data、text/plain三个值,其他的不行)

简单请求时,浏览器会直接发送跨域请求,并在请求头中携带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网站的跨域请求),同时也表明了这是一个跨域请求

  • Request的Headers(OPTIONS Method)
    • Origin

对于OPTIONS请求,我们关注这三个请求头,这三个请求头部到达服务端后,服务端必须设置这三个请求头部作为response的header返回给浏览器 浏览器根据Response中的这三个请求头部来判断服务端是否允许此次请求

  • Response的Headers(OPTIONS Method)
    • Access-Control-Allow-Origin
    • Access-Control-Request-Method
    • Access-Control-Request-Headers

解决跨域

正确的跨域流程

当跨域请求失败时,通常可以通过浏览器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

让后端支持CROS

方案一:让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‘]

可以看到,核心思想都是一致的,都是修改预先验证请求的规则来达到允许跨域请求的效果。

参考资料

  • 同源策略
    • https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
  • Python代码部分
    • http://www.bubuko.com/infodetail-2474505.html
  • 阮一峰 - 《跨域资源共享 CORS 详解》
    • http://www.ruanyifeng.com/blog/2016/04/cors.html

[[replyMessage== null?"发表评论":"发表评论 @ " + replyMessage.m_author]]

account_circle
email
web_asset
textsms

评论列表([[messageResponse.total]])

还没有可以显示的留言...
[[messageItem.m_author]] [[messageItem.m_author]]
[[messageItem.create_time]]
[[getEnviron(messageItem.m_environ)]]
[[subMessage.m_author]] [[subMessage.m_author]] @ [[subMessage.parent_message.m_author]] [[subMessage.parent_message.m_author]]
[[subMessage.create_time]]
[[getEnviron(messageItem.m_environ)]]
目录