0x00 前言
相信每一个前端 er 对于跨域这两个字都不会陌生, 在实际项目中应用也是比较多的. 但跨域方法的多种多样实在让人目不暇接. 老规矩, 碰到这种情况, 就只能自己总结一篇博客, 作为记录.
0x01 什么是跨域
跨域一词从字面意思看, 就是跨域名嘛, 但实际上跨域的范围绝对不止那么狭隘. 具体概念如下: 只要协议, 域名, 端口有任何一个不同, 都被当作是不同的域. 之所以会产生跨域这个问题呢, 其实也很容易想明白, 要是随便引用外部文件, 不同标签下的页面引用类似的彼此的文件, 浏览器很容易懵逼的, 安全也得不到保障了就. 什么事, 都是安全第一嘛. 但在安全限制的同时也给注入 iframe 或是 ajax 应用上带来了不少麻烦. 所以我们要通过一些方法使本域的 js 能够操作其他域的页面对象或者使其他域的 js 能操作本域的页面对象(iframe 之间). 下面是具体的跨域情况详解:
URL 说明 是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js 同一域名下 允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js 同一域名下不同文件夹 允许
http://www.a.com:8000/a.js
http://www.a.com/b.js 同一域名, 不同端口 不允许
http://www.a.com/a.js
https://www.a.com/b.js 同一域名, 不同协议 不允许
http://www.a.com/a.js
http://70.32.92.74/b.js 域名和域名对应 ip 不允许
http://www.a.com/a.js
http://script.a.com/b.js 主域相同, 子域不同 不允许(cookie 这种情况下也不允许访问)
http://www.a.com/a.js
http://a.com/b.js 同一域名, 不同二级域名(同上) 不允许(cookie 这种情况下也不允许访问)
http://www.cnblogs.com/a.js
http://www.a.com/b.js 不同域名 不允许
这里我们需要注意两点:
如果是协议和端口造成的跨域问题 "前台" 是无能为力的;
在跨域问题上, 域仅仅是通过 "URL 的首部" 来识别而不会去尝试判断相同的 ip 地址对应着两个域或两个域是否在同一个 ip 上.
("URL 的首部" 指 window.location.protocol +window.location.host, 也可以理解为 "Domains, protocols and ports must match".)
0x02 通过 document.domain 跨域
前面说过了, 浏览器有一个同源策略, 其限制之一是不能通过 ajax 的方法去请求不同源中的文档. 第二个限制是浏览器中不同域的框架之间是不能进行 js 的交互操作的. 不同的框架之间是可以获取 window 对象的, 但却无法获取相应的属性和方法. 比如, 有一个页面, 它的地址是 http://www.damonare.cn/a.html , 在这个页面里面有一个 iframe, 它的 src 是 http://damonare.cn/b.html, 很显然, 这个页面与它里面的 iframe 框架是不同域的, 所以我们是无法通过在页面中书写 js 代码来获取 iframe 中的东西的:
- <script type="text/javascript">
- function test(){
- var iframe = document.getElementById('ifame');
- var win = document.contentWindow;// 可以获取到 iframe 里的 window 对象, 但该 window 对象的属性和方法几乎是不可用的
- var doc = win.document;// 这里获取不到 iframe 里的 document 对象
- var name = win.name;// 这里同样获取不到 window 对象的 name 属性
- }
- </script>
- <iframe id = "iframe" src="http://damonare.cn/b.html" onload = "test()"></iframe>
这个时候, document.domain 就可以派上用场了, 我们只要把 http://www.damonare.cn/a.html 和 http://damonare.cn/b.html 这两个页面的 document.domain 都设成相同的域名就可以了. 但要注意的是, document.domain 的设置是有限制的, 我们只能把 document.domain 设置成自身或更高一级的父域, 且主域必须相同.
在页面 http://www.damonare.cn/a.html 中设置 document.domain:
- <iframe id = "iframe" src="http://damonare.cn/b.html" onload = "test()"></iframe>
- <script type="text/javascript">
- document.domain = 'damonare.cn';// 设置成主域
- function test(){
- alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
- }
- </script>
在页面 http://damonare.cn/b.html 中也设置 document.domain:
<script type="text/javascript">
document.domain = 'damonare.cn';// 在 iframe 载入这个页面也设置 document.domain, 使之与主页面的 document.domain 相同
</script>
修改 document.domain 的方法只适用于不同子域的框架间的交互.
0x03 通过 location.hash 跨域
因为父窗口可以对 iframe 进行 URL 读写, iframe 也可以读写父窗口的 URL,URL 有一部分被称为 hash, 就是 #号及其后面的字符, 它一般用于浏览器锚点定位, Server 端并不关心这部分, 应该说 HTTP 请求过程中不会携带 hash, 所以这部分的修改不会产生 HTTP 请求, 但是会产生浏览器历史记录. 此方法的原理就是改变 URL 的 hash 部分来进行双向通信. 每个 window 通过改变其他 window 的 location 来发送消息(由于两个页面不在同一个域下 IE,Chrome 不允许修改 parent.location.hash 的值, 所以要借助于父窗口域名下的一个代理 iframe), 并通过监听自己的 URL 的变化来接收消息. 这个方式的通信会造成一些不必要的浏览器历史记录, 而且有些浏览器不支持 onhashchange 事件, 需要轮询来获知 URL 的改变, 最后, 这样做也存在缺点, 诸如数据直接暴露在了 url 中, 数据容量和类型都有限等. 下面举例说明:
假如父页面是 baidu.com/a.html,iframe 嵌入的页面为 google.com/b.html(此处省略了域名等 url 属性), 要实现此两个页面间的通信可以通过以下方法.
a.html 传送数据到 b.html
a.html 下修改 iframe 的 src 为 google.com/b.html#paco
b.html 监听到 url 发生变化, 触发相应操作
b.html 传送数据到 a.html, 由于两个页面不在同一个域下 IE,Chrome 不允许修改 parent.location.hash 的值, 所以要借助于父窗口域名下的一个代理 iframe
b.html 下创建一个隐藏的 iframe, 此 iframe 的 src 是 baidu.com 域下的, 并挂上要传送的 hash 数据, 如 src="http://www.baidu.com/proxy.html#data"
proxy.html 监听到 url 发生变化, 修改 a.html 的 url(因为 a.html 和 proxy.html 同域, 所以 proxy.html 可修改 a.html 的 url hash)
a.html 监听到 url 发生变化, 触发相应操作
b.html 页面的关键代码如下:
- try {
- parent.location.hash = 'data';
- } catch (e) {
- // ie,chrome 的安全机制无法修改 parent.location.hash,
- var ifrproxy = document.createElement('iframe');
- ifrproxy.style.display = 'none';
- ifrproxy.src = "http://www.baidu.com/proxy.html#data";
- document.body.appendChild(ifrproxy);
- }
proxy.html 页面的关键代码如下 :
- // 因为 parent.parent(即 baidu.com/a.html)和 baidu.com/proxy.html 属于同一个域, 所以可以改变其 location.hash 的值
- parent.parent.location.hash = self.location.hash.substring(1);
0x04 通过 HTML5 的 postMessage 方法跨域
高级浏览器 Internet Explorer 8+, chrome,Firefox , Opera 和 Safari 都将支持这个功能. 这个功能主要包括接受信息的 "message" 事件和发送消息的 "postMessage" 方法. 比如 damonare.cn 域的 A 页面通过 iframe 嵌入了一个 google.com 域的 B 页面, 可以通过以下方法实现 A 和 B 的通信
A 页面通过 postMessage 方法发送消息:
- window.onload = function() {
- var ifr = document.getElementById('ifr');
- var targetOrigin = "http://www.google.com";
- ifr.contentWindow.postMessage('hello world!', targetOrigin);
- };
postMessage 的使用方法:
otherWindow.postMessage(message, targetOrigin);
otherWindow: 指目标窗口, 也就是给哪个 window 发消息, 是 window.frames 属性的成员或者由 window.open 方法创建的窗口
message: 是要发送的消息, 类型为 String,Object (IE8,9 不支持)
targetOrigin: 是限定消息接收范围, 不限制请使用 '*
B 页面通过 message 事件监听并接受消息:
- var onmessage = function (event) {
- var data = event.data;// 消息
- var origin = event.origin;// 消息来源地址
- var source = event.source;// 源 Window 对象
- if(origin=="http://www.baidu.com"){
- console.log(data);//hello world!
- }
- };
- if (typeof window.addEventListener != 'undefined') {
- window.addEventListener('message', onmessage, false);
- } else if (typeof window.attachEvent != 'undefined') {
- //for ie
- window.attachEvent('onmessage', onmessage);
- }
同理, 也可以 B 页面发送消息, 然后 A 页面监听并接受消息.
0x05 通过 jsonp 跨域
刚才说的这几种都是双向通信的, 即两个 iframe, 页面与 iframe 或是页面与页面之间的, 下面说几种单项跨域的 (一般用来获取数据), 因为通过 script 标签引入的 js 是不受同源策略的限制的. 所以我们可以通过 script 标签引入一个 js 或者是一个其他后缀形式(如 php,jsp 等) 的文件, 此文件返回一个 js 函数的调用.
比如, 有个 a.html 页面, 它里面的代码需要利用 ajax 获取一个不同域上的 json 数据, 假设这个 json 数据地址是 http://damonare.cn/data.php, 那么 a.html 中的代码就可以这样:
- <script type="text/javascript">
- function dosomething(jsondata){
- // 处理获得的 json 数据
- }
- </script>
- <script src="http://example.com/data.php?callback=dosomething"></script>
我们看到获取数据的地址后面还有一个 callback 参数, 按惯例是用这个参数名, 但是你用其他的也一样. 当然如果获取数据的 jsonp 地址页面不是你自己能控制的, 就得按照提供数据的那一方的规定格式来操作了.
因为是当做一个 js 文件来引入的, 所以 http://damonare.cn/data.php 返回的必须是一个能执行的 js 文件, 所以这个页面的 php 代码可能是这样的(一定要和后端约定好哦):
- <?php
- $callback = $_GET['callback'];// 得到回调函数名
- $data = array('a','b','c');// 要返回的数据
- echo $callback.'('.json_encode($data).')';// 输出
- ?>
最终, 输出结果为: dosomething(['a','b','c']);
如果你的页面使用 jquery, 那么通过它封装的方法就能很方便的来进行 jsonp 操作了.
<script type="text/javascript">
$.getJSON('http://example.com/data.php?callback=?,function(jsondata)'){
// 处理获得的 json 数据
});
</script>
jquery 会自动生成一个全局函数来替换 callback=? 中的问号, 之后获取到数据后又会自动销毁, 实际上就是起一个临时代理函数的作用.$.getJSON 方法会自动判断是否跨域, 不跨域的话, 就调用普通的 ajax 方法; 跨域的话, 则会以异步加载 js 文件的形式来调用 jsonp 的回调函数.
JSONP 的优缺点
JSONP 的优点是: 它不像 XMLHttpRequest 对象实现的 Ajax 请求那样受到同源策略的限制; 它的兼容性更好, 在更加古老的浏览器中都可以运行, 不需要 XMLHttpRequest 或 ActiveX 的支持; 并且在请求完毕后可以通过调用 callback 的方式回传结果.
JSONP 的缺点则是: 它只支持 GET 请求而不支持 POST 等其它类型的 HTTP 请求; 它只支持跨域 HTTP 请求这种情况, 不能解决不同域的两个页面之间如何进行 JavaScript 调用的问题.
0x06 通过 CORS 跨域
CORS(Cross-Origin Resource Sharing)跨域资源共享, 定义了必须在访问跨域资源时, 浏览器与服务器应该如何沟通. CORS 背后的基本思想就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通, 从而决定请求或响应是应该成功还是失败. 目前, 所有浏览器都支持该功能, IE 浏览器不能低于 IE10. 整个 CORS 通信过程, 都是浏览器自动完成, 不需要用户参与. 对于开发者来说, CORS 通信与同源的 AJAX 通信没有差别, 代码完全一样. 浏览器一旦发现 AJAX 请求跨源, 就会自动添加一些附加的头信息, 有时还会多出一次附加的请求, 但用户不会有感觉.
因此, 实现 CORS 通信的关键是服务器. 只要服务器实现了 CORS 接口, 就可以跨源通信.
平时的 ajax 请求可能是这样的:
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("POST", "/damonare",true);
xhr.send();
</script>
以上 damonare 部分是相对路径, 如果我们要使用 CORS, 相关 Ajax 代码可能如下所示:
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://segmentfault.com/u/trigkit4/",true);
xhr.send();
</script>
代码与之前的区别就在于相对路径换成了其他域的绝对路径, 也就是你要跨域访问的接口地址.
服务器端对于 CORS 的支持, 主要就是通过设置 Access-Control-Allow-Origin 来进行的. 如果浏览器检测到相应的设置, 就可以允许 Ajax 进行跨域的访问. 关于 CORS 更多了解可以看下阮一峰老师的这一篇文章: 跨域资源共享 CORS 详解
CORS 和 JSONP 对比
JSONP 只能实现 GET 请求, 而 CORS 支持所有类型的 HTTP 请求.
使用 CORS, 开发者可以使用普通的 XMLHttpRequest 发起请求和获得数据, 比起 JSONP 有更好的错误处理.
JSONP 主要被老的浏览器支持, 它们往往不支持 CORS, 而绝大多数现代浏览器都已经支持了 CORS).
CORS 与 JSONP 相比, 无疑更为先进, 方便和可靠.
0x07 通过 window.name 跨域
window 对象有个 name 属性, 该属性有个特征: 即在一个窗口 (window) 的生命周期内, 窗口载入的所有的页面都是共享一个 window.name 的, 每个页面对 window.name 都有读写的权限, window.name 是持久存在一个窗口载入过的所有页面中的, 并不会因新页面的载入而进行重置.
比如: 我们在任意一个页面输入
- window.name = "My window's name";
- setTimeout(function(){
- window.location.href = "http://damonare.cn/";
- },1000)
进入 damonare.cn 页面后我们再检测再检测 window.name :
window.name; // My window's name
可以看到, 如果在一个标签里面跳转网页的话, 我们的 window.name 是不会改变的.
基于这个思想, 我们可以在某个页面设置好 window.name 的值, 然后跳转到另外一个页面. 在这个页面中就可以获取到我们刚刚设置的 window.name 了.
由于安全原因, 浏览器始终会保持 window.name 是 string 类型.
同样这个方法也可以应用到和 iframe 的交互来:
比如: 我的页面 (http://damonare.cn/index.html) 中内嵌了一个 iframe:
<iframe id="iframe" src="http://www.google.com/iframe.html"></iframe>
在 iframe.html 中设置好了 window.name 为我们要传递的字符串.
我们在 index.html 中写了下面的代码:
- var iframe = document.getElementById('iframe');
- var data = '';
- iframe.onload = function() {
- data = iframe.contentWindow.name;
- };
Boom! 报错! 肯定的, 因为两个页面不同源嘛, 想要解决这个问题可以这样干:
- var iframe = document.getElementById('iframe');
- var data = '';
- iframe.onload = function() {
- iframe.onload = function(){
- data = iframe.contentWindow.name;
- }
- iframe.src = 'about:blank';
- };
或者将里面的 about:blank 替换成某个同源页面(about:blank,javascript: 和 data: 中的内容, 继承了载入他们的页面的源.)
这种方法与 document.domain 方法相比, 放宽了域名后缀要相同的限制, 可以从任意页面获取 string 类型的数据
0x08 后记
其它诸如中间件跨域, 服务器代理跨域, Flash URLLoader 跨域, 动态创建 script 标签 (简化版本的 jsonp) 不作讨论.
来源: https://www.cnblogs.com/JetpropelledSnake/p/9445079.html