关于前端跨域解决方案

什么是跨域?什么是跨站资源共享?什么是CROS?

跨域与同源策略

1. 什么是域?什么是同源

一个url(统一资源定位符)的基本组成部分: 协议 :// 域名(或者是ip) : 端口(默认80) / 路径
判断同源的三要素是:协议、端口、域名

2. 与域有关的浏览器动作:

- 1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

3. 什么是同源策略?

同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送

不受同源限制的情况:
1.) script标签允许跨域嵌入脚本,稍后介绍的JSONP就是利用这个“漏洞”来实现。
2.) img标签、link标签、@font-face不受跨域影响。
3.) video和audio嵌入的资源。
4.) iframe载入的任何资源。(不是iframe之间的通信)
5.) 的插件。
6.) WebSocket不受同源策略的限制。

4. 如何获取(get)和设置(set)域(domain)

get: document.domain;
set: document.domain=”XXXX” (赋值改写)
//初始值 “home.example.com”
document.domain = “example.com”; //OK
document.domain = “home.example.com”; //NO,不能由松散变紧绷
document.domain = “example”; //NO,必须有一个点号
document.domain = “another.com”; //NO, 必须是有效域前缀或其本身

同源策略之cookie、iframe、cros介绍

1. 关于Cookie和Session:

说到同源策略,必不可少的就是Cookie这个东西了。

而讲到Cookie,跟它关联在一起的又有Session。

对于这两者,具体去传送门查阅

做个小结:

  • Cookie受到浏览器同源策略的限制,A页面的Cookie无法被B页面的Cookie访问和操作。
  • Cookie最大存储容量一般为4KB,由服务器生成,在HTTP报文首部设置Set-Cookie可以指定生成Cookie的内容和生命周期,如果由浏览器生成则在关掉浏览器后失效。
  • Cookie存储在浏览器端,由于每次发送HTTP请求默认会把Cookie附到HTTP首部上去,所以Cookie主要用来身份认证,而不用来存储其他信息,防止HTTP报文过大。
  • Session存储在服务器,主要与Cookie配合使用完成身份认证和状态保持的功能。只有Cookie或只有Session都无法完成身份认证和状态保持的功能。
session和cookie做状态保持和身份验证————

假设现在有一个学生信息管理系统,此时数据库已经有学生的相关信息。(账号、密码、个人信息等等)
然后当学生登录这个系统,通过POST请求把用户的账户密码发送到后台服务器。当后台服务器接收到这些参数的时候,会跟数据库保存的记录进行匹配。
一旦匹配成功,也就是用户的账号密码都正确的情况下,这个时候后台服务器会在Session中记录一个值,可以是用户名或者其他能够唯一标识用户的字段。
当把这个值保存在Session中后,后台服务器会返回响应告知客户端登录成功,可以进行后续的操作。此时,后台服务器会在HTTP响应报文中添加一个字段Set-Cookie,它的值是当前Session的SessionID,(这个SessionID是指向我们当前的那个Session的,在Node的Express中express-session会封装好这个过程)当然还会设置Cookie的其他属性,比如说过期时间Expires等等。
当浏览器接收到这个HTTP响应报文的时候,就会在本地设置一个Cookie,它的过期时间由响应报文中Set-Cookie中的Expires字段的值决定,如果为空,则关闭浏览器(即会话结束时)后失效。
之后,每次向后台服务器发送请求的时候,浏览器默认会把这个Cookie加在HTTP请求报文的Cookie中。这样,每次后台服务器接收到请求的时候,会根据Cookie中的SessionID去找到我们的Session。
假如这个SessionID映射得到Session,那么这个时候说明浏览器是已经登录过了。于是,就可以进行后续的一些相关的操作。
另外,值得一提的是,Session机制决定了当前客户只会获取到自己的Session,而不会获取到别人的Session。各客户的Session也彼此独立,互不可见。也就是说,当多个客户端执行程序时,服务器会保存多个客户端的Session。获取Session的时候也不需要声明获取谁的Session。

这就是Cookie和Session做状态保持和身份验证的一个具体的例子。

2. 关于iframe:

同源限制有一个不得不提的就是iframe,iframe可以在父页面中嵌入一个子页面,在日常开发中一旦使用,避免不了的就要涉及到不同的iframe页面进行通信的问题,可能是获得其他iframe的DOM,或者是获取其他iframe上的全局变量或方法等等。

同源下的iframe,也就是iframe中的src属性的URL符合同源的条件,那么通过iframe的contentDocument和contentWindow获取其他iframe的DOM或者全局变量、方法都是很简单的事情。

那如果是非同源的两个iframe,单纯的通过变量访问的方式就受到同源限制了。

非同源的两个iframe,单纯的通过变量访问的方式就受到同源限制了。为了解决这个问题,HTML5引入了一个新的API:###postMessage###,主要就是用来解决存在跨域问题的iframe页面之间通信的问题。

postMessage API

postMessage接受两个参数,一个是要传送的data,另外一个是目标窗口的源,如果想传给任何窗口,可以设置成*。
目标页面接收信息的时候,使用的是window.addEventListener(“message”, function(e) {})。

把B页面用iframe嵌在A页面下面的栗子(A-http://localhost:4002/parent.html,B-http://localhost:4003/child.html)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- A-http://localhost:4002/parent.html -->
<body>
<h1>A页面</h1>
<iframe src="http://localhost:4003/child.html" id="child">
</iframe>
<script>
window.onload = function() {
document.getElementById("child").contentWindow.postMessage("父页面发来贺电", "http://localhost:4003");
}
</script>
</body>
<!-- B-http://localhost:4003/child.html -->
<body>
<h1>B页面</h1>
<script>
window.onload = function() {
window.addEventListener("message", function(e) {
// e.data
//判断信息的来源是否来自于父页面,保证信息源的安全
if(e.source != window.parent) return;
alert(e.data);
});
};
</script>
</body>

3. CORS-跨域资源共享:

  1. 简单请求和非简单请求
    简单请求:
  • 请求方法:HEAD、GET、POST之一;
  • 请求头 Content-Type只限于三个值(表单提交):application/x-www-form-urlencoded、multipart/formdata、text/plain。
  1. 非简单请求:是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
    一旦浏览器通过了“预检”,以后每次浏览器正常的CORS请求,都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都有一个Access-Control-Allow-Origin头信息字段。如果开启了Cookie设置,那还有一个Access-Control-Allow-Credentials:true。

需要浏览器和服务器同时支持。
原理:使用”Origin:”请求头和”Access-Control-Allow-Origin”响应头来扩展HTTP。其实就是利用新的HTTP头部来进行浏览器与服务器之间的沟通。

  • 针对前端代码而言,变化的地方在于相对路径需改为绝对路径。

    1
    2
    3
    4
    5
    6
    7
    8
    //以前的方式
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "/test", true);
    xhr.send();
    //CORS方式
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "http://segmentfault.com/test", true);
    xhr.send();
  • 针对服务器代码而言,需要设置Access-Control-Allow-Origin,列出源或使用通配符来匹配所有源。
    优点:
    CORS支持所有类型的HTTP请求。
    使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据
    不足:
    不能发送和接收cookie
    更新:服务端可以通过设置Access-Control-Allow-Credentials该字段来表示是否允许发送Cookie。发送ajax请求时,需配置withCredentials属性。
    不能使用setRequestHeader()设置自定义头部
    兼容IE10+

CORS不能发送和接收cookie解决办法:

服务器返回的响应的与CORS有关的几个头信息字段:

Access-Control-Allow-Origin: http://hello.world.com或者*【必须】
Access-Control-Allow-Credentials: true 【可选】它的值是一个布尔值,表示是否允许发送Cookie
Access-Control-Expose-Headers: FooBar 【可选】XMLHttpRequest对象的getResponseHeader()

方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
ORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。另一方面,开发者必须在AJAX请求中打开withCredentials属性。

1
2
3
4
5
6
// server
Access-Control-Allow-Credentials: true
// client
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

更详细的CROS,请移步阮一峰的《跨域资源共享》一文

常用跨域解决方案

1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域

通过jsonp跨域

通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// client 动态创建script,再请求一个带参网址实现跨域通信
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参并指定回调执行函数为onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);
// 回调执行函数
function onBack(res) {
alert(JSON.stringify(res));
}
</script>
// server 服务端返回如下(返回时即执行全局函数)
onBack({"status": true, "user": "admin"})

参考文献:

  1. https://segmentfault.com/a/1190000011145364
  2. https://segmentfault.com/a/1190000006908944
  3. https://segmentfault.com/a/1190000012256432