跨域-理论篇

浏览器限制一个源的文档(document)或它加载的脚本(script)如何与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。但是这样也造成问题:跨域。在前后端分离的项目中,前端发送请求给后台服务器,后台服务器接收到请求,处理完响应给前端。但是由于浏览器的同源策略(Same-origin policy),该响应被拦截了。

什么是同源

要了解同源,先来看看 URL 是怎么组成的:

protocol://[host]:[port][path][?query][anchor]

e.g.

http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument

其中:

  • protocol 为协议,这里为 http
  • host 为域名,这里为 www.example.com。其中 example.com 为主域名,www.example.com 为子域名。
  • port 为端口,这里为 80
  • path 为资源路径,这里为 /path/to/myfile.html
  • query 为查询字符串,这里为 ?key1=value1&key2=value2
  • anchor 为描点,这里为 #SomewhereInTheDocument

在 URL 中,只要 protocol, hostpath 相同,则认定为同源。

例如,一个 URL 为 http://store.company.com/dir/page.html 下面列出对该源的比较:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/other.html 失败 主机不同
http://company.com/dir/other.html 失败 主机不同,storecompany.com 的一个子域名。

同源策略

浏览器认为互联网是不可信的,于是它引入了同源策略,拒绝非同源的请求。

浏览器共限制三种行为:

  • Cookie、LocalStorage 和 IndexDB 只能同源读取。

    其中,Cookie 的同源策略有点不同。可以为父域(e.g. example.com)设置 Cookie,那样其子域(e.g. www.example.com, abc.example.com)都共享该 Cookie。当然也可以只为子域设置 Cookie,这样该 Cookie 只能子域访问。

    这个比较好理解,自己源的数据只能自己读取。要是别人都能读取,那信息不久全部泄露了。

  • DOM 只能同源获得。

    也就是两个不同源的页面之间无法对方的 DOM 元素。如果该限制取消了,就会造成安全隐患。最经典就是 iframe 的例子:

    你在某处打开了这个网站 https://www.bilibil.com/,这个网站看起来和哔哩哔哩没有任何区别,但是仔细观察它并不是官方网站。黑客使用 iframe 加载真正的哔哩哔哩网站,并将 iframe 布局调整全屏,看起来就和真正的哔哩哔哩没有任何区别。而你也没有察觉这样的问题,于是你就登录你的账号。如果没有 DOM 同源策略,黑客就可以从 https://www.bilibil.com/ 网站直接抓取输入框的 DOM,从而拿到你的账号和密码。

  • XMLHttpRequest 只能同源请求。

    XMLHttpRequest 也就是我们常说的 AJAX。AJAX 的同源可以免受浏览器遭受到 CSRF 攻击(并不能抵挡全部的 CSRF 攻击)。

这里主要讨论同源策略中不同源之间的请求、交互限制。

同源策略中对不同源之间的交互限制,比如主要分为三类:

  • 跨域写操作(Cross-origin writes) 一般是被允许的例如,链接(links),重定向以及表单提交。
  • 跨域资源嵌入(Cross-origin embedding) 一般是被允许。例如,一系列可以通过 src 引入资源的标签:scriptimgvideoaudio 等;通过 @font-face 引入的字体;通过 iframe 载入的任何资源。
  • 跨域读操作(Cross-origin reads) 一般是不被允许的。例如,AJAX。

解决方案

解决跨域,一般就是解决不同源之间可以互相请求 AJAX,不同源之间可以互相获取 DOM 节点。

AJAX 解决方案

在说明解决方案之前,来看看浏览器是如何阻止 AJAX 跨域的。

  1. 我们在当前网站中请求一个非同域的 API。
  2. 浏览器就会给该服务器发送请求。
  3. 服务器接收到请求后,处理过后成功响应。
  4. 浏览器接受到这个响应,发现该请求为跨域响应,于是拒绝了该响应。

从上面得出结论,浏览器并不是不能发送跨域请求,而是拒绝了跨域响应。

这也是我们在请求非同源网站时,浏览器会返回 net::ERR_FAILED 并告诉我们并没有设置 Access-Control-Allow-Origin 这个头信息。

解决方案一:CORS

经过多年的发展,前后端分离已成为业界标杆。既然是前后端分离就要处理跨域问题。

我们知道,在服务器返回响应给浏览器后,浏览器是拒绝了该响应。所以 W3C 就推出一个标准:CORS。它是通过设定一系列的响应头来解决跨域问题的。

最重要的响应头当然是 Access-Control-Allow-Origin,它可以设置成 * 代表允许所有网站的请求跨域。也可以设置成 URI,只允许指定的网站请求跨域。

这里列举常用的响应头,更详细的内容可以查看 MDN 的文档

  • Access-Control-Allow-Origin 请求资源能共享给那些域
  • Access-Control-Allow-Credentials 是否允许发送 Cookie
  • Access-Control-Allow-Methods 对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。
  • Access-Control-Max-Age 预请求结果能被缓存多久,单位为秒。-1 表示禁用缓存。

因为是响应,所以 CORS 的解决方案在后端进行。

对了,CORS 分为 简单请求非简单请求 这里不过多赘述,详情请看文档

解决方案二:代理服务器

我们知道同源策略是浏览器上的限制,服务器之间是没有跨域这个说法的。

所以可以加个中间层来解决这个问题:

  1. 浏览器的请求发到同源代理服务器
  2. 同源代理服务器收到请求后进行转发,转发到不同源的服务器。以此进行数据交互。
  3. 数据交互之后,先发送到同源代理服务器,再转发到浏览器。

整个过程浏览器只和同源代理服务器交流,故解决跨域。

解决方案三:JSONP

JSONP 可以堪称是跨域解决的奇淫技巧,因为它是利用 <script> 元素的「漏洞」来实现的。

从上面我们可以知道,<script> 标签是可以引用其他域的 js 脚本,该 js 脚本载入成功后会触发回调函数,该回调函数里面就有我们想要的数据。

使用 JSONP 的好处为浏览器兼容性好,但它只能支持 GET 请求。

解决方案四:WebSocket

WebSocket 和 HTTP 一样都是应用层的协议,与 HTTP 不一样的是,它并没有同源策略。只要服务器支持该协议,就可以实现跨域通信。

获取 DOM 解决方案

我们知道 iframewindow.open 打开的窗口,他们与父窗口之前无法通信。父窗口无法获取子窗口的 DOM,反之亦然。

document.domain

通过 document.domain 这个属性值,如果两个窗口的一级域名相同,则可以施行窗口之间的通信。同样可以通信的还有 Cookie 值。

片段标识符(fragment identifier)

片段标识符指的是 URL 后面 # 号后面的部分,也成为锚(anchor)。

如果只是改变片段标识符的内容,页面就不会重新刷新,从而实现跨域。

window.name

windows.name 是浏览器窗口的一个属性。该属性,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。

window.postMessage

上面两种方法都是利用的漏洞,HTML 5 引入跨文档通信 API(Cross-document messaging)来解决这个问题。

这个 API 为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源。

它可以解决一下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

window.postMessage 这个方法非常强大,详情请参考 文档

参考链接

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!