Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

聊聊跨域 #4

Open
yleo77 opened this issue Apr 26, 2015 · 0 comments
Open

聊聊跨域 #4

yleo77 opened this issue Apr 26, 2015 · 0 comments

Comments

@yleo77
Copy link
Owner

yleo77 commented Apr 26, 2015

聊聊跨域

这篇文章是 2013 年在搜狐视频时为分享的一个 Topic 写的,主要从两个场景:获取跨域服务器相关资源和纯客户端页面和页面之间的跨域信息获取 以及在这两个场景下都有哪些技术方案可以选择。

因为 2014 年底和近期又遇到过两次和跨域有关的 Case,而且还是同一个, 所以就把这个 Case 当做一个知识点补充进来。

不存在完美的方案,只有适合特定场景的方案。

什么是跨域?

简单说就是因为 javascript 的同源策略限制,当域名不同时,安全考虑则禁止了彼此间的一个通信。

什么情况下会造成跨域?

协议不同,端口不同,子域不同,都会造成跨域

什么情况下需要跨域?

前端交互开发中,往往需要往不同于当前域名的后端服务器获取/提交一些数据,或者是不同 window 之间的一个通信,但因为同源限制,导致 javascript 开发者不得不采取一些措施去解决这一技术问题,以下分别就这两个场景做一些技术总结。

与服务器的交互通信

JSONP

因为 DOM 中可以插入第三方 javascript 文件并执行,所以利用这一特性,服务器在返回的数据上 wrap 一层函数名,以 javascript 函数调用的形式返回给客户端。

优点:

  • 最通用解决方案
  • 灵活,对于开放性 API,可通用以 JSONP 形式提供访问
  • 从内部协同角度来讲,后端的接口也可伸缩,可根据前端 Querystring 来决定接口返回 JSON 还是 JSONP 的数据.
  • 适用面广,应该说绝大场景都可以利用该方案,不过需要后端配合

缺点:

  • 当产生大量 script 节点时,需要考虑在 DOM 树中进行移除,否则可能产生内存泄露

需要注意的一个地方,其本质是通过往 head 里插入一个 script 节点,来让外域的 js 执行达到跨域这样一个目的,所以如果是一个频繁来获取数据的情况下,需要再该 js 节点执行完之后,移除该节点。

移除该节点的同时,其占用的内存事实上没有释放的,还需要删除一下该节点上的属性。一个最佳实践是最好创建一个 script 节点,然后去一次次改变它的 src 属性。via

另外,再移除节点的时候,需要注意 head 里是否有 base 节点的情况,如果存在 base 节点 并且是自闭合的写法的话,最好把 script 插在 head.firstChild 位置,或者移除节点的时候通过 script.parentNode.removeChild(script) 来完成,否则 IE6 会报错。 via

后端解决

前端代码不做修改,还是以 ajax 形式从同域服务器获取数据,后端同学从 server 端去获取数据,然后返回给前端标准的 JSON 数据,这个情况是没有域这个概念。

优点:

  • javascript 不用做修改

缺点:

  • 需要后端增加新的接口支持,意味着一定的沟通成本。

服务器配置

举个栗子

Nginx

location /cross_domain_api/ {
    proxy_pass http://other.domain.com
}

Apache

# 加载 依赖的模块
LoadModule proxy_module modules/mod_proxy.so   
LoadModule proxy_http_module modules/mod_proxy_http.so

# 配置
ProxyRequests Off
ProxyPass /cross_domain_api http://other.domain.com

生产环境的话,就需要根据各种不同的情况来进行配置,同时增加一些 header 的配置。

优点:

  • 省时省力,开发人员不用在代码层面上做出调整.
  • 需要对服务器做出修改. 需要对服务器比如 apache,nginx 配置有所了解 。

缺点:

  • 服务器配置需要小心,有些坑需要注意, 比如 地址上多一个 / 可能就会有意外的惊喜。

XHR 2

HTML5 新增的特性,只要被请求的目标服务器配置 header

     Access-Control-Allow-Origin: *

就可以了。这个的值写法比较奇葩,必须写 HTTP,还不支持通配符。

优点:

  • 非常简单,不需要修改代码,简易配置服务器便可
  • get/post 都可以

缺点

  • IE. IE9 以下(包含 IE9 )都不支持.

2015-04-26 更新
如果在一个不需要支持低版本浏览器的环境下,这个方案目前来看非常完善,但是有一个关于缓存的很重要的问题,需要注意:

问题现状是这样,在客户端缓存了一个需要跨域获取的资源时,因为第一次加载需要和服务器交互,服务器返回了跨域头,一切都正常,符合预期。但是在该资源未过期的前提下,当用户通过敲回车的形式加载页面再次请求该文件时,客户端不会向服务器发请求,直接返回200(from cache)的信息,但又因为该资源是需要跨域头的,现在本地的缓存是没有这个头,所以会直接报出跨域错误的信息。这个一定要注意。(文章开头提到的两个 Case 发生的原因就是这个,一个case 是因为用 Ajax 来获取的 js 资源,结果缓存后再通过 Ajax 获取发现是大量的 status 0,跨域错误的,导致页面出不来;另一个是 ajax 获取数据。)

补充的一点,ie虽说不支持标准,但勉为其难得ie8, 9支持 XDomainRequest 对象,权且当做对开发者的一点安慰吧。link

    var xdr = new XDomainRequest();
    xdr.open('get', 'url');
    xdr.onload = function(){}
    xdr.send();

最后再补充一个小点,关于客户端在获取一个跨域资源时是否会和服务器端进行一次交互的问题,答案是。客户端当识别这是个跨域资源时,会验证服务器的Response Header 中是否有关于跨域头的设置,如果没有就抛出跨域错误,如果有则资源正常加载。

页面间的跨域通信

从域名的角度来分两种情况, 跨子域,跨主域。

从使用场景来分,父窗口和子 iframe 之间的通信,以及 tab 和 tab 之间的通信。tab 和 tab 之间的通信有一个前置条件为 其中一个 tab 必须由前一个 tab 打开,也就是依赖一个句柄引用。

关于 iframe location 的读写限制,与导航相关属性的操作限制如下:

  • window.location.href 可写不可读
  • window.location 除href之外的属性 禁止
  • document 禁止
  • iframe.src 可写可读

那关于 iframe 想读父窗口的 location.href 怎么办

  • 父窗口是 top 窗口的情况, 直接通过top.location.href 来读
  • 父窗口不是 top 的话,通过 parent.location 是不能访问的,这里有个 trick, 通过 document.referrer 可以取到(备注: 这个方法不是完全之策)。

跨子域

在两个需要通信的页面上设置

document.domain = 'sohu.com'

优点

  • 对于一些小比较简单的网站系统,比较适合推荐, 简单,省事,省力
  • 浏览器都支持

缺点, 太多了

  • 安全问题,比如像第三方的开发工具在子域名下,被加载进来
  • 不适用于大型页面较多的网站,parent 如果设置了 document.domain,那么所有的 iframe 也必须设置,否则无法交互,这是一个潜在的大坑。
  • 对于像低版本ie 678, 当设置了 document.domain 后,会有一系列问题
    • 获取 location.href 可能会抛异常. via
    • ie6 下 ajax 的前进后退。iframe也必须设置 document.domain。之前在看 cowboy 的 hashchange 组件,1.3的 update 更新记录,就是为了解决这个问题,在 iframe 中也设置了 document.domain。via
    • 编辑器,如果设置了的话,ie 下无法使用

结论:先知其弊而避之;再知其弊而用之。

location.hash + iframe

背景是这样:父页面 parent.com/p.html 和子页面 child.com/c.html 通信,c.html 需要向 p.html 传递数据。

思路: 此时需要在子页面中添加一个和 p.html 同域的 iframe 代理页面 meditor.html,数据通过该代理 iframe 的 location hash 进行传递。

优点

  • 主域子域都可搞定

缺点

  • 数据单向传递, 只能从子传递至父窗口
  • 数据量传递大小有限制. 对比各个主流浏览器下,ie 的最大长度最小,为 2083. via 如果太长了,服务器会抛出 414 (Request-URI Too Long) 的状态码
  • 部分场景比如非持续性传递数据的话, 往往需要去监听 hashchange 事件或者依赖一个 setInterval 的轮训机制。前者呢,ie8+支持,后者呢性能有损耗.

对于数据量比较大的情况,可以通过建立多个 iframe 来进行传递,举个栗子,c.html要传递 abcdef 长度为6的字符串,假设浏览器每次对 iframe 携带数据长度限制为2,怎么办呢?既要保证数据没有遗漏,又要保证在数据被拆分后,又能按照原始位置进行拼接完好?

也就是说 每个 iframe 都需要携带三部分数据: 1. 数据总长度; 2. 当前数据片要插入的位置;3. 数据分片.

所以,分解后的数据传递应该是这个样子

    <iframe src="parent.com/meditor.html#3_0_ab"></iframe>
    <iframe src="parent.com/meditor.html#3_1_cd"></iframe>
    <iframe src="parent.com/meditor.html#3_2_ef"></iframe>

之后装载数据便可。

    // 2014-04-26 修改
    // 一次组装
    var ret = [];
    var index = 0;
    var data = location.hash.slice(1).splice('_');
    ret[data[1]] = data[2];
    index++;

    // 当 ret 长度等于 data[0]时组装完成
    if(index == data[0]){
        // callback
    }

window.name + iframe

当对一个页面设置 window.name 后,即使改变该页面 url,window.name 也不会被重写。所以借助这一特点,可以实现跨域的数据传递。

// 2014-04-26 更新
关于 window.name 的一点补充

改写顶层页面 window.name 值后,通过改变页面 url,对于后续页面

  • 如果前后两个页面为同域时,window.name 不改变,可以获取到
  • 如果前后两个页面为不同域,window.name 被置为默认值。

另外一种情况,当设置子 iframe 中 window.name,当改变 iframe 的 location 后

window.name 不会因为改变页面 url (包括域不同)而恢复为初始值。当然了,如果父窗口想读取该值,那需要子 iframe 和父为同源。

这里,便是借助第二种情况这个特点,来实现跨域的数据传递。

背景没变,父页面parent.com/p.html和子页面child.com/c.html通信,c.html 需要向p.html传递数据。

原理大概是这样:在 c.html 中设置window.name的值 ,值为需要传递给 p.html的数据。设置完毕,p.html 重写 iframe.src, 使新的 src 指向和 p.html 为同一个域名的一个空白代理页, 当 iframe 加载完毕,读取该 iframe.contentWindow.name (这个时候已经是同域了).

优点

  • 支持大数据量,据说可以达到 2mb,所以某些网站甚至用它来做缓存
  • 比较安全,不会像因为因为 iframe 而有某些隐患。
  • 比较快
  • 技术上比较简单

postMessage

HTML5 新增的一个技术,IE 的支持情况呢 IE 分两个阶段 IE8+ 和 IE10+ 来讲。

  • IE8, IE9 支持 iframe 的 postMessage。via, 不支持 window.open 形式打开的页面.
  • IE10 同域下支持,跨域下不支持,抛出No such interface supported
  • IE9 以下支持传string,新版本也可以传对象了

对于第一点的不足,这里 其实也是给出了一些实现上的方法

另外说说 firefox

  • 6.0 以下版本也是仅仅支持 string 类型的
  • 8.0 以上版本支持文件对象的发送

flash

需要在网站根目录或相关目录下放置一个 crossdomain.xml 文件

    <cross-domain-policy>
         <!-- domain 是域名, secure 是是否加密访问 -->
         <allow-access-from domain="*.sohu.com" secure="false" />
    </cross-domain-policy>

具体配置可以在网上搜搜

优点:

  • 支持良好,无兼容性问题。

缺点:

  • 依赖 flash 插件, 比如移动端可能就支持受限了。
  • 团队角度来看,需要会 flash 的开发人员

一些topic

跨域 post

两种情况

如果并不很在意跨域 post 提交结果的返回值,(比如,在能保证网络链接正常和程序无 bug的情况下,post 提交都会返回一个正常值。) 这个情况可以简单用 setTimeout 来搞

如果 post 提交会出现很多种情况,这种情况下,需要借助服务器端的跳转来完成。大致可以这样去解决:增加中转页面a.com/meditor.html, a.com 的页面提交数据通过 iframe 提交至 b.com 服务端后,服务端处理完后通过跳转到 a.com/meditor.html 并在中转页面的 location.hash 上附带服务器端处理结果,此时中转页就可以通过调用 parent 的函数来完成业务逻辑。

img 和 script 的 crossorigin

对 img 标签来说,增加了该属性,可以允许其他地方使用该图片,这样描述不太准确,想象一下 canvas.drawImage 可以根据一个图片来绘制,就是这个。当为第三方域的图片时,如果不加该属性,虽然说可以 drawImage 出来,但是当后续想访问该 canvas 的一些 function 时,例如我调用 toDataURL, toBlob, getImageData, 就会抛出一个SecurityError的异常

    Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported

也是为了避免未经授权的图片信息不正当使用。

对于 script 标签来说,添加该属性后,可以将该脚本的一些信息暴露出来,比如 onerror 事件, 捕捉该事件时,同域的话,可以拿到相关信息,比如 message,line,type, 但跨域默认是拿不到的,只会抛出一个 Script error.

解决办法: 加了这个属性就可以拿到了。 当然了,需要搭配服务器返回 Access-Control-Allow-Origin: * 的 header

css 是否加载完成的判断

老版本的 firefox 中,当判断跨域的 css 文件是否加载完成用到的一个 error 事件,原理是当 css 加载完成并尝试访问node.sheet.cssRules时,会抛出NS_ERROR_DOM_SECURITY_ERR的这么一个 error,基于此便可以确定该不同域的 css 是否加载完成.

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant