这篇我们聊聊跨域和同源策略。

跨域

如果有一些前端经验,我们都多少知道跨域是什么意思,或者至少有一个模糊的理解;我们可以简单的理解为从www.baidu.com这个网站的也页面上发起了一个向www.google.com的请求来获取资源;从一个狭义的角度我们完全可以这样去定义跨域。我对跨域的广义定义(不一定对,大家讨论)是:通过安全或不安全对某种策略来通过浏览器同源策略的限制,实现从不同与当前页面域名的地址上获取资源的一种机制。

我们可以在MDN上看到对于跨域资源共享(CORS: corss origin resource sharing)这种机制的标准定义和详细介绍。所以一定程度上我们可以把针对于cors的工作理解为跨域的大部分工作。

几个概念

与跨域相关的问题,有这几个核心概念饶不开: 同源策略,CORS,jsonp,iframe。我们逐个来聊一下。

同源策略

MDN上是这样定义同源策略的:

如果两个 URL 的 protocol、port (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。

在这个定义中往往容易让人(包括我)忽视的反而是第一个protocol,举例来说http://baidu.comhttps://baidu.com是不同源的,仔细观察一个是http协议,一个是https。

浏览器是对跨域进行分类的

跨域具体分:写操作、资源嵌入、读操作。

  • 对于写操作MDN上的表述令人困惑,只是简单提到链接、表单提交等几个字。这里举例子来说,如果用户访问了恶意网站,而恶意网站提交的表单向某个内网站点A发起请求,但是A页面依然需要身份认证,这就是CSRF攻击的基础。当然我们也有很多策略来防御CSRF,比如说recapture,Referer检查,以及业界标准ctoken机制。因此在同源策略里,大部分的跨域写操作是被允许的,而这里的允许指的是仅仅是浏览器不拦着你,但并不代表你请求的服务器不拦着你;就像你从自己家去小明家,你家大门你冲出去了,但小明家未必给您开门;

  • 跨域嵌入也是允许的。具体就是在html上我们看到的那些需要外部资源引入的页面元素,比如<img /><script />等等。由于懒惰,我就直接截图放在下面好了。

image.png

  • 跨域读,这个仍然比较难理解。举例子来说你在http://aaa.com上,尝试去请求一个用户在另一个地址http://bbb.com上的信息,若这个地址的资源需要你的一些用户信息,比如登陆态、cookie,默认在这次跨域请求里是不带过去的;如果默认带过去的话,浏览器就实在默认泄露用户隐私,让用户的安全处于无防护状态;请求的http://bbb.com的服务可以毫不费力的获取用户的登陆状,然后利用登陆状态进行恶意攻击,比如帮你在微博关注几个营销号。而如果代码中我们确定要跨域访问某个服务怎么办呢?尤其是现在微服务盛行的情况下,在请求的options中,加入withCredentials: true的选项,你的cookie中的登陆状态就会随着这次请求发送到目标服务了,与之对应的是服务器端的Access-Control-Allow-Credentials字段,但这都属于CORS的内容了。

至此,我们已经弄明白,在浏览器视角,是如果划分写操作、资源嵌入、读操作三种跨域的了。除此之外,我们在代码中(js)发起的请求,不管是fetch还是xhr,都会收到同源策略的限制。

CORS

既然知道了浏览器的同源策略,我们就开始考虑怎么和不同源的服务进行资源交换,CORS就是其中一种方式。而且是成本比较低且比较现代化的方式。

通过CORS跨域的主要工作在于服务端设置好跨域请求头,并且在前后端配合时至少有一个兄弟弄明白cors规则跨域怎么搞。在cors方案中对跨域又分了两类,简单请求(simple request)和非简单请求(not-so-simple request)。他们的区别是非简单请求有一个preflight过程,其实就是在发送一个http method为Options的请求,若这次请求成功,才会把真正跨域的http请求发送出去。那如何判断是不是简单请求,这里又偷懒截图了:

image46236305c1fb853b.png

在服务端返回的response中,这几个头影响了cors收否成功:

  • Access-Control-Allow-Origin: <origin> | *: 指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。在阮一峰老师的博客中也提到一点是很容易被忽视的:如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名,而这个cookie又涉及到同源政策,因此有时候这两部分是相互影响的,出问题也不太容易想得到。
  • Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-HeaderAccess-Control-Allow-Headers: <field-name>[, <field-name>]*: xhr可以从getResponseHeader中拿出来的header;用来告诉option请求,接下来跨域时允许的用户自定义header
  • Access-Control-Allow-Credentials: 在聊同源策略的读操作时我们提过了,这个用来设置服务器是否允许请求带cookie,这个要配合http的withCredentials: true一起使用。
  • Access-Control-Allow-Methods: 用来告诉option请求,接下来的跨域请求可以使用的http method

在cors的最后,让我来放一个cors失败的截图,我们看到浏览器报错这种信息的时候,记得查一下到底cors哪里失败了。

  • 发起测
    • 多加的header没有在服务端allow?
    • origin没有allow?
    • 非简单请求preflight失败还是真正请求失败?
  • 接受测
    • 非简单请求有没有加响应option的路由?
    • 是否allow了正确的origin?
    • 剩下那几个头挨个看看?
      cors failed

jsonp

一种古老且难用的跨域实践。对于jsonp网上有各路大神已经进行了全方位无死角的讲解;在这里我只简单记录下。

原理:利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show(‘我不爱你’)。
最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

iframe

iframe是一个经典的跨域方案。最简单的应用是我们可以把B站点作为A站点的一部分嵌入进去,然后一起提供给用户,这样A站点既包含了自己的资源,也能为用户提供B站点的资源渲染。

基于iframe的能力上,又有人发明了更加hack的使用方式来实现跨域的数据请求,这种情况只是为了跨域获取数据而不在乎document。

  • window.name: 这种方法是非常古老的,这个name属性直接挂在到了window上,而且可以跨越页面访问,不仅可以实现夸页面通信,也可以结合iframe来进行跨域数据请求;它在iframe的src属性变更时,保存在里面的内容并不会丢失,且这个属性的上限是2M的数据容量。具体可以看这里
  • location.hash
  • document.domain。这种情况比较在做ssr的oauth登录时用过,用document.domain来实现子域通信。

但是上述这三种跨域都是hack的非主流方案,现代浏览器发展到今天,使用cors是比较面向未来的方案;而只有遇到一些陈旧项目时,我们可能会看到前人们为跨域所付出的这些努力。

postMessage

postMessage可以夸页面尽心通信,即便页面之间是不同源;因此可以实现跨域行为。这种行为是基于window的事件机制,接收方监听“message”事件;不能跨越不同浏览器使用;两个window基于这个api来实现通信。

比较常见的有两种scenario。一种是在一个页面中引入了iframe,iframe的src与宿主页面不同源,那iframe的contentWindow要与外层window资源交换,这就是一个跨域场景;另一种是在同一浏览器下,不同的活动页面通过postmessage互相发信息,这就是比较典型的用法了,不在这里展开聊。

其他方法

跨域还有一些比较边缘的方案,并不是说不重要,而是我觉得对于跨域这个定义来讲有点偏离主题。

webSocket也是一种非常好的且面向未来的方案,这个是基于长链接的,而且也并不再是基于http的协议,就不展开了。