从网络链路到跨域问题

首先,得知道什么是域。

再首先,得先知道Web服务从访问到收到数据并展现这个过程发生了什么。

IP

注:下文中 网络空间 = TCP/IP网络空间, IP = IP地址

IP本身是Internet Protocol的缩写,是一种为了计算机相互连接通信而产生的协议,我们在这就用它代指ip地址。

每个在线的网络服务在网络空间的真实存在形式都是以ip地址形式存在的,它属于网络七层模型中的网络层,它是一个地址,用来识别网络空间中互联的主机和路由器,暂且认为用ip地址可以访问到一个服务器。

好比地球上的每个地点都有一个经纬度,只要这个经纬度真实存在并有效可用,我们根据这个经纬度就一定能找到这个地点,且经纬度是永久不变的。

实际上ip地址的“永久不变”是指在这个ip对应的互联网服务的生命周期内,其不会变化。

直接服务场景:假设某公司拉了一条电信专线服务,电信给其分配8个ip地址,其中3个为广播地址,剩余5个配置且部署好对应的网络服务,外部是可以直接访问到该公司所架构的网络服务的。

真实商业生产场景:假设在某云服务厂商购买服务器,默认会分配有唯一ip,服务器重启、重置…ip是不会变化的,但服务器如果到期了,服务器提供商会同时释放掉ip资源,这个ip可能就会分配给其他服务,亦或者回收这个ip,ip本身和服务没有关系,但大多数的商业服务ip都是随服务捆绑的。

简单说:ip是指向网络空间具体某一处的唯一地址,但它并不是永远不变的。

域名和DNS

像经纬度一样,ip地址是一长串数字,不便于记忆,所以我们需要一个类似地名一样的别名,当我们一旦说出别名,就知道它大概在哪,且无需care它的真实经纬度。

我们之所以听到地名就知道这个位置大概所在,是因为我们大脑内已经存储了这个地名和真实地点的关联信息,暂且称之为我们存储的这块用于关联地点的数据为“数据库”。

人脑有限,我们不可能记住所有的地名和地理位置,所以需要有一个容易专门来存储这些关联数据的数据库,最好其可以直接把我们带到目的地。

这就是DNS,全称Domain Name System,他做的事情很简单,就是将我们输入的ip别名(域名)通过数据库解析为ip地址返回。

实际上,浏览器或我们发起请求的客户端会根据DNS返回的ip去请求网络资源,并返回解析展示。

然而,浏览器请求时如何知道域名使用的是哪家DNS服务商呢,于是浏览器便需要先把域名发送到本地配置的DNS服务商(本地域名服务器/Local DNS Server)那里得到该域名的NS(Name Server),然后再把该域名拿到Name Server去获取IP,然后再向该IP请求数据。

实际上整个域名解析的链路十几步不止,浏览器请求LDNS之前会对浏览器本身的DNS缓存和本机DNS缓存的判断,判断会根据命中情况和TTL和其他数据决定是否进入下一步。

LDNS如果查询失败,则会直接请求root DNS Servers,root DNS Servers只为全球只有十三台的gTLD(generic Top-Level DNS Server)进行服务,RDNS会返回域名所在的主域名服务器的地址,即对应的gTLD地址,然后继续向gTLD发送请求以得到NS地址,于是又回到了上一步。

一张图来表示:

《从网络链路到跨域问题》

简单说就是:DNS是一套完整的系统,这套系统做的事就是根据一个个的表去查对应的数据,最终返回一个目标IP。

跨域

该说域了;我们可以把域理解为一个域名或IP所代表的范围,比如访问a.com指向了A服务器,访问b.com指向了B服务器,我们可以认为a和b是分开独立的两个域。

大多数情况下,a.com b.com都应该是两个没有关系的单独网络服务,他们默认不应该产生关联(静态资源引用除外),起码浏览器是这么认为的。

所以如果你在a.com下向b.com发起一个触发浏览器安全机制的xhr的网络请求,浏览器默认是会拦截的,并附赠一大串Error,他认为你这么做不安全,不允许跨域请求数据。

CORS

CORS(Cross-origin resource sharing)是一个W3C标准,全称”跨域资源共享”。
它规定允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而解决跨域问题。

我们先看浏览器的安全机制是怎样的?

浏览器把异步请求(xhr/fetch)分为两类:

  • 简单请求
  • 非简单请求

简单请求的条件:

  1. 请求方法是以下三种方法之一:

    • HEAD
    • GET
    • POST
  2. HTTP的头信息不超出以下几种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

只要不满足以上条件的请求均为非简单请求

简单请求

简单请求在发出时,浏览器会自动给请求头加上Origin字段,该字段为当前请求发出者所在的域(协议 + 域名 + 端口),
服务端根据请求headers里的Origin来判断是否允许请求者获取资源,如果允许,服务端在返回时会在headers里携带几个特殊的字段,用于告知浏览器,允许此次请求,
否则,即使正常返回数据,浏览器检测到无对应的允许跨域字段,也会在console throw Error,告知你跨域访问失败。

这几个字段便是CORS标准中所实现的三个字段:

  1. Access-Control-Allow-Origin
    该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

  2. Access-Control-Allow-Credentials
    该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
    如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

  3. Access-Control-Expose-Headers
    该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里指定。

非简单请求

非简单请求一般出现在对服务端进行CUD操作的场景下,异源RESTful就是一种最典型的场景,会使用到PUTDELETEPATCH…等请求类型,且一般以application/json格式进行数据交互。

当浏览器把一个请求判定为非简单请求,则其发出前,浏览器会预先对服务端发起一个OPTIONS类型的预检(preflight)请求,同时也会加上Origin字段,
浏览器会根据此次预检请求返回的响应头来判断,服务端是否允许本域跨域操作资源,若判断为允许,则浏览器立即发出本身要发出的请求,否则,控制台抛出异常,中断请求。

服务端应返回的响应头应包含以下几个CORS字段:

  1. Access-Control-Allow-Origin
    必需返回,同简单请求中的含义。

  2. Access-Control-Allow-Methods
    该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

  3. Access-Control-Allow-Headers
    如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

  4. Access-Control-Allow-Credentials
    可选返回,同简单请求中的含义。

  5. Access-Control-Max-Age
    该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

在每一次真正的数据请求时,Access-Control-Allow-Origin字段都是服务端一定会返回的。

为避免频繁的预检请求降低效率,真实生产环境下,建议设置Access-Control-Max-Age字段。

解决方法

开发环境下解决方案

通过本机开启代理实现在开发环境下将api转化为子路径,Node.js、apache、nginx均可实现。

webpack版本代码: 全部代码

proxy: {
    '/api': {
        target: 'http://localhost:8000',
        secure: false,
        changeOrigin: true,
        pathRewrite: {
            '^/api': ''
        }
    }
},复制代码

生产环境下解决方法

为服务端设置预检请求的响应及相关的CORS字段。Node.js版本代码

误区

JSONP只是在CORS未规范之前用于解决基本跨域的曲径(奇技淫巧),其并非解决跨域的真正途径。

web开发者在使用vue-resource、axios…等各种异步库时,可能会存在类似解决跨域的选项,其可能是内部对简单请求和非简单请求进行的一些基本转换,此类并非真正解决跨域的方法,解决跨域务必需要服务端处理。

本文部分内容参考来源:

《跨域资源共享 CORS 详解》

MDN – HTTP访问控制(CORS)

W3C – HTTP/1.1: Method Definitions

原文地址:surmon.me/article/21

    原文作者:小蜜蜂
    原文地址: https://juejin.im/post/58f9eda9570c350058cd6be4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞