跨域与跨域访问

479 阅读12分钟

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

前端开发中跨域是一个绕不过的问题,跨域的方法也是千奇百怪。浏览器都有一个同源策略,其限制之一就是不能通过ajax的方法去请求不同源中的文档。 它的第二个限制是浏览器中不同域的框架之间是不能进行js的交互操作的。

先考虑几个问题:

Q:为什么会有跨域问题

A:浏览器的同源策略限制,浏览器会拒绝跨域请求。

在web的交互环境中,只能保证请求发自某个用户的浏览器,但是不能保证请求本身就是用户自愿发出的。这个时候就就有构造跨站请求伪造(CSRF)可能,这里利用的就是网站对浏览器的信任。 严格来说,浏览器并不是拒绝所有的跨域请求,它拒绝的其实是跨域的读操作。浏览器的同于策略是这样执行的:

  • 浏览器允许跨域写操作(Cross-origin writes),如链接,重定向
  • 浏览器允许跨域资源嵌入(Cross-origin embedding),如img,script标签
  • 浏览器不允许跨域读操作(Cross-origin reads)

Q:为什么浏览器只限制读,不限制写呢

A:假如连请求都发不出去,那么在源头上就限制死了,网站之间无法共享资源了。一般限制了读操作,拦截了浏览器的请求结果,就已经可以限制黑网站根据请求结果继续下一步的操作了。同源策略可以通过csrf攻击绕过。

Q:什么情况算跨域

A:非同源请求,均为跨域。

Q:为什么有跨域需求

A:工程服务化后,不同职责的服务分散在不同的工程中,这些工程的域名往往是不同的,但是一个需求可能需要对应到多个服务,这时便需要调用不同服务的接口,因此会出现跨域。

跨站请求伪造(CSRF)

有同源策略的限制,跨域的Ajax请求不会带上cookie,但是script/iframe/img等标签却是支持跨域的。所以在请求的时候会带上cookies。

栗子一

CSRF攻击的主要目的是让用户在不知情的情况下攻击自己已登录的一个系统,类似于钓鱼。

  • 用户当前已经登录了邮箱,或bbs,如A.com,这时cookie中已经有了token。
  • 用户打开已经被入侵者控制的站点,如B.com,我们姑且叫它钓鱼网站。
  • 在B.com上构建一个源为A.com的iframe,加载时,iframe会执行对A.com的一些操作。
  • 由于当前你的浏览器状态已经是登陆状态,所以session登陆cookie信息都会跟正常的请求一样,纯天然的利用当前的登陆状态,让用户在不知情的情况下,帮你发帖或干其他事情。

栗子二

假如一个网上路由器的配置教程网页里,被人加了一个img标签如下:

其中ip是大部分路由器的配置地址,这个图片虽然没有显示出来,但是请求确发出去了,这个请求可以给路由器添加一个vpn代理,指向入侵者的代理服务器。假如路由器的验证也放在cookie中,那么这个vpn请求可能就成功了。

同源策略

同源指两个页面拥有相同的协议(protocol)、端口(port)和主> 机(host),那么这两个页面属于同一个源(origin)

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

跨域访问

如何实现跨域呢?

iframe跨域

这个主要针对浏览器的第二种限制的。

这里需要按照不同的场景来区分:不跨域;主域相同,子域不同;主域不同。当然不跨域的情况就不用说了,可以通过父窗口通过contentWindow访问iframe,iframe通过parent访问父级,通过top访问顶级。

子域跨父域

通过document.domain跨域,这个一般父子域才会使用。我们只能把document.domain设置成自身或更高一级的父域,且主域必须相同。这时可以和父域进行交互,但是向父域发送请求的时候还是会跨域的,这种更改domain只是支持client side,不是client to server。

修改document.domain的方法适用于不同子域的框架间的交互。

主域不同

通过window.name跨域

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

注意,window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。而且即使打开的页面是不同域的,上述结论依旧适用。

通过location.hash跨域

hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分),A通过location.hash方式传递参数给B,B通过定时器检测hash变化,执行对应操作。动态改变location.hash,iframe不会重载。无论跨域与否,iframe内可以获取自己的location.hash。

图像Ping

在CORS技术出现以前,要实现Ajax跨域通信是比较困难的。开发人员们想出了一些办法,利用DOM中能够执行跨域请求的功能,在不依赖XHR对象的情况下也能发送某种请求。例如图像ping和JSONP。

我们知道,一个网页可以从任何网页加载图像,不用担心跨域不跨域,所以,我们就可以利用图片不受“同源限制”这一点进行跨域通信。 我们利用JS创建一个新的Image对象,并把src属性设置为指向请求的地址,通过监听onload和onerror事件来确定是否接受到了响应。响应的数据可以是任意内容,但通常是像素图或204响应。

这种方式优点是很明显的:兼容性非常好,缺点就是:只能发生GET请求,而且无法获取响应文本。

通过jsonp跨域

利用动态脚本插入技术,动态创建script标签,同样是利用src属性,向指定URL发出请求。也就是说,服务器返回的数据就是要插入文档的js代码。在客户端监听onload和onerror事件。 这里要注意的是,不同于img元素,script标签要插入文档后才开始下载。jsonp由两部分组成:回调函数和数据。回调函数是当响应到来时用该在与面调用的函数,数据是由服务器填充的传入回调函数的json数据.

jsonp的本质

JSONP本质上就是让数据变成js代码,使用script标签来加载数据。

如我的用户数据api本来是

https://www.x.com/api/v4/members/jack

返回值为

{"name": "jack", "gender": 1}

想要通过JSONP实现在y.x.com下跨域拿到这个数据,这个时候明显需要跨域,而且是非父子域,需要做的是:

  • 改造这个api,让他返回一段js,而不是json数据
可以修改为如果你访问:
https://www.x.com/api/v4/members/jack?callback=render
得到的响应为:
render({"name": "jack", "gender": 1})
   ```
* 在数据使用页面,不使用ajax去拿取数据,而是嵌入一个script标签:
* 在数据使用页面写好一个叫render的函数,用来渲染用户数据

因为浏览器并不限制script标签的src是要从哪里加载脚本,跨域问题似乎就被JSONP「绕过」了。

#### JSONP的限制
再想一想,浏览器不做script来源的跨域限制,而且大家都喜欢用JSONP并且改造了大量的api响应,问题不是回到了原点吗?我有一个网站a.com,在里面嵌入了支付宝某个api的JSONP版本(也就是个script);我骗你访问a.com;浏览器自动去加载script,也就去访问了这个api。

其实问题并没有回到原点,因为JSONP实际上受限很大。作为一个script标签,
* **一是浏览器只会使用GET方法去请求它**
* 二是请求它的时候不会携带cookie
* 三是能被改造成JSONP形式的api一定是纯粹用来GET数据的。

就算其他网站用这些JSONP改造过的接口,也不会对网站造成影响。

**所以后端开发者最好不要在GET操作里做非幂等的事,因为别人在他的网站里嵌入script或者img标签放你网站的url,浏览器就会发出一个不带cookie的GET请求。**

但是前端开发中,总有场景需要复杂的跨域需求。比如我就是需要在y.x.com页面下,发post请求到www.x.com域名下的api,而且还是要带cookie的。这时JSONP就完全用不上了。

相比于图像Ping,JSONP的优势在于:可以能够直接访问响应文本,支持在浏览器和服务器之间的双向通信。 缺点是:JSONP直接从其他域加载代码执行,如果其他域不安全,可能会在响应中夹带一些恶意代码。其次,要确定JSONP请求是否失败并不容易,HTML5为script增加了onerror方法,但是目前支持度还不是很好。

### 通过CORS跨域
JSONP更像一种hack,是一种非标准行为,利用了script标签来做数据的事情,并且JSONP的使用使得别人可以在他自己的网站上使用你的数据(当然假如别人使用后端代理的手段还是可以的)。

对于跨域的访问控制,是有HTTP标准的。这也是网上很多讲跨域的文章的主要内容,我就只简单介绍,跨域资源共享(CORS)把跨域行为分三类:简单请求、预检请求、带cookie的请求

#### 简单请求
如简单的GET和POST。

还是以y.x.com页面请求www.x.com的api为例。
* 浏览器发出请求时,request里会带上Origin头,值为y.x.com
* 这时只需要api响应header里带的`Access-Control-Allow-Origin`字段包含(匹配)了发送请求的页面所在的域名(y.x.com),浏览器就会认为合法,把数据接着使用。
* 否则,浏览器会拦截掉这段数据:没错,响应的数据已经放body里到达了客户端,而浏览器会阻止掉,让专栏页面里负责发ajax的那段js代码拿不到响应值。

这样的好处很明显:我只需要在服务器端(通常是网关这一层)配置好Access-Control-Allow-Origin,而我的代码逻辑不需要对来源站点区别对待,就阻止其他人纯前端的手段使用我的数据,做到HTTP访问控制。

#### 预检请求
略微复杂一定的请求,如PUT和DELETE等,或者请求时添加了CORS安全的header之外的header(如自定义的)。

这时,正式发送跨域请求前,浏览器会先对目标api发出一个OPTIONS预检请求,这个请求里会带三个和跨域相关的header,其值为预检之后,正式发送api请求时将会使用的来源/方法/请求头。这三个header是:
* Origin
* Access-Control-Request-Method
* Access-Control-Request-Headers

看名称应该能大概知道对应什么了。预检请求的响应需要带着与它们对应匹配的header和值,这样浏览器才会去请求跨域api。

预检请求的出现,是因为PUT等复杂操作通常是非幂等的。如果像简单请求一样直接请求,发现响应不合理才去拦截响应值,这个时候后端的PUT操作里该执行的事情已经被执行过了。

**至于为什么POST这个非幂等语义的方法会是简单请求,我觉得应该是历史包袱。毕竟在CORS出现前,form表单里POST就是能跨域使用的。而早期的js很弱小,提交form之后页面会刷新跳转到目标地址,源地址是拿不到POST响应的数据的**

#### 带cookie的请求
这种跨域请求才是最危险的,最严重情况下能实现上面举的支付宝转账例子。

所以这种请求要求响应头里`Access-Control-Allow-Credentials`为true,且`Access-Control-Allow-Origin`不能是通配符,防止后端开发者犯错。

关于CORS更具体的规则,可以在MDN查阅到详细的资料。


### ### 通过HTML5的postMessage方法跨域
window.postMessage(message,targetOrigin)  方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。

FB的第三方授权使用的就是这种。

发送方:window.postMessage
* 第一个参数message为要发送的消息,类型只能为字符串;
* 第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,如果不想限定域,可以使用通配符 * 。

```js
window.parent.postMessage('hello world','*');

接收方

window.onmessage(fun(e){})
// 或者
window.addEventListener('message',function(e){
        console.log(e.data);        //hello world
        console.log(e.origin);      //http://127.0.0.1:8020 所传来数据的域
    })

web sockets

web sockets是一种浏览器的API,它的目标是在一个单独的持久连接上提供全双工、双向通信。(同源策略对web sockets不适用)

总结

除了跨域,还需要注意点击劫持等行为

以上! 最后的惯例,贴上我的博客,欢迎关注

参考资料 JS中的跨域问题 Ajax 跨域难题 - 原生 JS 和 jQuery 的实现对比 为什么给你设置重重障碍?讲一讲Web开发中的跨域

请关注公众号:全栈飞行中队