写在开头

老生常谈的问题,网上的总结也有很多,重要的不是总结,而是把所有的方法都尝试一遍。

纸上得来终觉浅,绝知此事要躬行

概念:只要协议、域名、端口有任何一个不同,都被当作是不同的域。

URL 说明 是否允许通信
http://www.a.com/a.jshttp://www.a.com/b.js 同一域名下 允许
http://www.a.com/lab/a.jshttp://www.a.com/script/b.js 同域名不同文件夹 允许
http://www.a.com:8000/a.jshttp://www.a.com/b.js 同一域名,不同端口 不允许
http://www.a.com/a.jshttps://www.a.com/b.js 同一域名,不同协议 不允许
http://www.a.com/a.jshttp://70.32.92.74/b.js 域名和域名对应ip 不允许
http://www.a.com/a.jshttp://script.a.com/b.js 主域相同,子域不同 不允许
http://www.a.com/a.jshttp://a.com/b.js 同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.jshttp://www.a.com/b.js 不同域名 不允许

跨域

浏览器出于安全的考虑(避免恶意网站轻易读取其他网站显示的内容数据等)原则上允许跨域写而限制了跨域读。写是指数据的上行/发送( sending request ),读是指数据的下行/接受( receiving response )。

跨域在实际生产中可分为三类:

  1. 与服务器端通信有跨域问题
  2. 不同页面间的 iframe 通信会有跨域问题
  3. 单点登录时的 Cookie 认证会有跨域问题

AJAX的跨域请求

这里用 express 模拟一个服务器(3000端口),在其他端口通过 AJAXexpress 发送 GET 请求,可以看到熟悉的错误信息:

chrome

express

express 服务器是可以收到 GET 请求的,只不过在 response 的时候被浏览器拦截。
(然而跨域写也是很不安全的,容易导致 CSRF/clickjacking 攻击。浏览器已经限制了跨域读,再限制跨域写的话,那网络上的每个页面都变成了孤岛)

iframe通信跨域

在做这个演示之前,需要本地模拟不同域环境

1
2
3
4
5
# 编辑/etc/hosts文件
# 新增配置
127.0.0.1 sub.a.com
127.0.0.1 www.a.com
127.0.0.1 www.b.com

先启动一个 express 服务器,用于模拟不同的域名环境以及静态资源调用,对 iframe 跨域尝试的所有 demo 都在 这个仓库,这里就不截图了。

Cookie 相关文档信息中,提到过 Cookie 是不能跨域访问的,但是在二级域名是可以共享 Cookie 的。这样我们在开发多个项目并且想使用单点登录时就有了限制,必须将多个系统的域名统一,作为二级域名,统一平台提供使用主域名,这也算解决方案之一。

Cookie 信息跨域,暂时能想到三种方法:

  1. 通过服务端提供一个获取当前域下所有 Cookie 的请求地址,服务端将获取的 Cookie转成 js 代码,在其他域下使用 jsonp 跨域加载该 js 代码。
  2. p3p 协议
    response.Header('P3P','CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR')
    兼容性不是很好。
  3. 通过 URL 参数实现 token 信息传递

对单点登录的认识不够深刻,后面做单点登录时再重新总结。

对于以上的跨域类型,特别注意的两点:

  1. 对于端口和协议的不同,只能通过后台来解决。
  2. 在跨域问题上,域仅仅是通过“URL 的首部”来识别,而不会去尝试判断相同的 ip 地址对应着两个域或两个域是否在同一个 ip 上。
    “URL 的首部”指 window.location.protocol + window.location.host

解决方案

document.domain

对于主域相同而子域不同的例子,可以通过设置 document.domain 的办法来解决。

document.domain 默认的值是整个域名,所以即使两个域名的二级域名一样,那么他们的 document.domain 也不一样。

使用方法就是将符合上述条件页面的 document.domain 设置为同样的二级域名。这样我们就可以使用其他页面的 window 对象引用做我们想做的任何事情了。

具体的做法是可以在http://www.a.com/main.htmlhttp://sub.a.com/test1.html两个文件中分别加上document.domain = 'a.com';然后通过a.html文件中创建一个iframe,去控制iframecontentDocument,这样两个 js 文件之间就可以“交互”了。

补充知识:

x.one.example.comy.one.example.com 可以将 document.domain 设置为 one.example.com,也可以设置为 example.com
document.domain 只能设置为当前域名的一个后缀,并且包括二级域名或以上(.edu.cn 这种整个算顶级域名)。

Fragment Identitier Messaging(FIM)

Fragment Identitier 就是URL的井号(#)后面的经常用于锚点定位的部分,这部分的改变不会导致页面刷新,母窗口可以随便访问 iframeURL,而 iframe 也可以随便访问母窗口的 URL,那这二者之间就可以通过改变Fragmement Identitier来实现通信了。缺点是Fragmement Identitier的改变会产生不必要的历史记录,而且也有长度限制;另外,有的浏览器不支持 onhashchange 事件。

还有一种变种是Cross Frame(CF),域间页面跳转,携带 URL 跳回到同域

具体做法是在 http://www.b.com/test2.html 中将所需的数据拼在 URL 的后面,而这个 URL 指向的地址http://www.a.com/sub_domain_pass.html需要和想要获取数据的http://www.a.com/main.html同域,利用通域间可以自由通信来做数据传递。

缺点:

  1. 需要借助中间页面跳转,麻烦
  2. 通过 URL 传递参数,直接暴露在外面,数据容量和类型都有限

HTML5 postMessage

HTML5 新增了跨文档消息传输 Cross Document Messaging兼容ie8以上

otherWindow.postMessage(message, targetOrigin);

  • otherWindow : 对接收信息页面的 window 的引用
    • 页面中 iframecontentWindow 属性
    • window.open 的返回值
    • 通过 name 或下标从 window.frames 取到的值
  • message : 所要发送的数据,string 类型
  • targetOrigin : 用于限制 otherWindow,“*”表示不作限制

document.name

看以下一个场景:
随意打开一个页面,输入以下代码:

1
2
window.name = "hello,world";
location.href = "http://www.baidu.com/";

再检测 window.name :

1
window.name; // hello,world

可以看到,如果在一个标签里面跳转网页的话,window.name 是不会改变的。
基于这个思想,我们可以在某个页面设置好 window.name 的值,然后跳转到另外一个页面。在这个页面中就可以获取到我们刚刚设置的 window.name 了。

由于安全原因,浏览器始终会保持 window.namestring 类型。

这个方法也可以应用到与 <iframe> 的交互上来。

在页面index.html中内嵌了一个 <iframe id="iframe" src="http://omg.com/iframe.html"></iframe>

iframe.html 中设置好了 window.name 为我们要传递的字符串。
index.html 中写了下面的代码:

1
2
3
4
5
6
var iframe = document.getElementById('iframe');
var data = '';

iframe.onload = function() {
data = iframe.contentWindow.name;
};

可结果往往不如意,两个页面完全不同源。
跨域

由于 window.name 不随着 URL 的跳转而改变,可以使用一个黑科技来解决这个问题:

1
2
3
4
5
6
7
8
9
var iframe = document.getElementById('iframe');
var data = '';

iframe.onload = function() {
iframe.onload = function(){
data = iframe.contentWindow.name;
}
iframe.src = 'about:blank';
};

或者将里面的 about:blank 替换成某个同源页面(最好是空页面,减少加载时间)。

补充知识:
about:blankjavascript:data: 中的内容,继承了载入他们的页面的源。
这种方法与 document.domain 方法相比,放宽了域名后缀要相同的限制,可以从任意页面获取 string 类型的数据。

JSONP(json with padding填充式 json)

看起来和json差不多,只不过是被包含在函数调用中的json。例如callback({hello:'world'});

我们知道,在页面上有三种资源是可以与页面本身不同源的。它们是:js脚本,css样式文件,图片。而 jsonp 就是利用了<script>标签可以链接到不同源的js脚本,来到达跨域目的。

当链接的资源到达浏览器时,浏览器会根据他们的类型来采取不同的处理方式,比如,如果是 css 文件,则会进行对页面 repaint ,如果是 img 则会将图片渲染出来,如果是 script 脚本,则会进行执行。

因此只要我们在请求页面定义回调函数,将 json 数据用函数名包装起来(形如函数调用),数据就传递给了原页面函数的形参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//请求页面
var script = document.createElement('script');
function test(arg){
console.log(arg);
}
script.onload = function(){
console.log(3);
}
script.src='./test.js';
document.getElementsByTagName('body')[0].appendChild(script);

//跨域页面或后台接口返回
console.log(1);
test({"data":123});

//依次打印 1 ,Object {data: 123} ,3

客户端在与服务端交互时,需要约定好回调函数名称,后端需要封装调用。但这种方式兼容性极好。
jsonp 只能以 GET 的方式进行跨域,如果想用其他方式进行跨域,就需要使用 CORS 了。

最后需要明确的一点是 jsonpAJAX 原理完全不同,即使在 jQuery 的实现里,如果发现参数 typejsonp,它内部也是通过创建 script 标签来请求数据。

CORS

  1. 请求发起时,浏览器先判断当前是否是跨域的 AJAX 请求
  2. 如果是,判断是否是简单请求
    • 只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-datatext/plain 中的一种。
    • 不会使用自定义请求头(类似于 X-Modified 这种)。
  3. 简单请求,直接发到服务端,在响应头中寻找 Access-Control-Allow-Origin,如果有且允许,处理响应结果。
  4. 对那些会对服务器数据造成破坏性影响的 HTTP 请求方法(特别是 GET 以外的 HTTP 方法,或者搭配某些 MIME 类型的 POST 请求),标准强烈要求浏览器必须先以 OPTIONS 请求方式发送一个预请求(preflight request),从而获知服务器端对跨源请求所支持 HTTP 方法。(要求服务端返回 Access-Control-Allow-MethodsAccess-Contorol-Allow-Headers
  5. 在确认服务器允许该跨源请求的情况下,以实际的 HTTP 请求方法发送那个真正的请求。服务器端也可以通知客户端,是不是需要随同请求一起发送信用信息(包括 CookiesHTTP 认证相关数据)。

兼容性

图片ping

上面我们说过,在页面上有三种资源是可以与页面本身不同源的。它们是:js 脚本,css 样式文件,图片。利用完 js 脚本,我们试试图片。

将图片的 src 属性指向请求的地址,通过监听 loaderror 事件,就能知道响应什么时候接受了,响应的数据可以是任意内容,但通常是像素图或204响应。图像ping的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//index.html
var btn = document.getElementById('start');
btn.onclick = function(){
var img = new Image();
img.onload = function(){
document.getElementById('result').innerHTML = '已经加载';
}
img.src = 'http://localhost:3000/img/st.gif';
}
//app.js
app.get('/img/st.gif',function(req,res){
console.log('get data');
res.send({
state:true
});
});

我们发现请求发送成功了,却没有获取响应文本。也就说他是单向通信。

这种方式优点是很明显的:兼容性非常好
缺点就是:只能发生 GET 请求,而且无法获取响应文本。
因此在实际生产当中会在 GET 请求后面拼上特征字段,用来埋点,日志统计。

flash跨域

它会访问目标网站根目录下面的 crossdomain.xml 文件,根据文件中的内容来确定是否允许此次跨域访问,基本没有人用了:

1
2
3
<cross-domain-policy>
<allow-access-from domain="xxx.xxx.com" />
</cross-domain-policy>

proxy

后台proxy这种方案牵涉到后台配置,这里就不阐述了,有兴趣的可以看看 yahoo 的这篇文章:《JavaScript: Use a Web Proxy for Cross-Domain XMLHttpRequest Calls》

参考

JavaScript 跨域总结与解决办法
Access_control_CORS