This website requires JavaScript.
BING
Because the mountain is there
OG

网络链路和跨域

5,330 words, 13 min read2017/03/15 AM5,113 views

首先,得知道什么是域。

再首先,得先知道 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 里携带几个特殊的字段,用于告知浏览器,允许此次请求, 否则,即使正常返回数据,浏览器检测到无对应的允许跨域字段,也会在控制台 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 版本代码: 全部代码

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
proxy: { '/api': { target: 'http://localhost:8000', secure: false, changeOrigin: true, pathRewrite: { '^/api': '' } } },

生产环境下解决方法

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

误区

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

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

本文部分内容参考来源:

Creative Commons BY-NC 4.0https://surmon.me/article/21
6 / 6 comments
Guest
Join the discussion...
  • h4j
    H4j🇨🇳CNGuangzhou ShiAndroidChrome WebView
    #173

    博主用的什么绘图工具?

    • Surmon
      Surmon🇨🇳CNYintaiMac OSChrome
      #174

      reply:

  • Jose
    Jose🇨🇳CNBeijingMac OSChrome
    #86

    看了一半,学习了,先加入收藏夹,剩下的明天再看

    • Surmon
      Surmon🇨🇳CNBaiyangMac OSChrome
      #87

      reply:

      🌹撒花

  • wilson
    Wilson🇨🇳CNXi'aniOSWeChat
    #84

    谷歌也有一款插件,允许跨域,适合开发使用。扒B站的数据用🌚,另外选完表情能不能让表情框xiao shg

    • Surmon
      Surmon🇨🇳CNFenyangAndroidWeChat
      #85

      reply:

      不错👏,移动端对hover等事件支持不是很好,所以可能会有所异常,实际上只要点击空白使添加表情按钮失焦就可以啦🌹