跨域
- 1、什么是跨域
- 2、跨域方法
- 2.1 JSONP
- 2.2 CORS
- 2.3 window.postMessage
- 2.4 window.name
- 2.5 document.domain
- 3、跨域方式间比较
1、什么是跨域
概念:只要不遵循同源策略的请求,都被当作是跨域的。
同源策略(same-origin policy):浏览器出于安全方面的考虑,只允许本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读取对方的资源。
这里的本域指:
- 同协议: 如均为
http
或https
协议 - 同域名: 如
github.com/jazen
和github.com/Tony
- 同端口: 如均为80端口或8080端口
也就是说只要协议、端口、域名这三者中有一者不同,则可以说是跨域的。
下面是一个关于JavaScript能否跨域通信的例子,以 http://www.aaa.com/a.js
访问以下URL的结果。见下表:
URL | 说明 | 是否允许通信 |
---|---|---|
http://www.aaa.com/b.js |
同一域名下 | √ |
http://www.aaa.com/script/b.js |
同一域名下,不同文件夹 | √ |
http://www.aaa.com:8080/b.js |
同一域名,不同端口 | × |
https://www.aaa.com/b.js |
同一域名,不同协议 | × |
http://127.0.0.1/b.js |
域名和域名对应的IP | × |
http://script.aaa.com/b.js |
主域相同,子域不同 | × |
http://aaa.com/b.js |
同一域名,不同二级域名(同上) | × |
http://www.bbb.com/b.js |
不同域名 | × |
对于端口和协议的不同,只能通过后台来解决。前端针对不同域名,则有以下几种解决方案。
2.1、JSONP
JSONP(JSON with Padding):一种用于解决AJAX跨域问题的方案。(把JSON包裹在回调函数中传回,这个padding就可以理解为这个回调函数)
原理: AJAX由于受到同源策略的限制无法跨域,但带有 src
属性的标签(例如<script>
、<img>
、<iframe>
)并不受同源限制(也应用这个特性来使用CDN)。因此可以通过向页面中动态添加 <script>
标签来完成对跨域资源的访问。
实现方式: 在网页添加一个 <script>
元素,向服务器请求JSON数据;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。具体步骤如下:
- 定义处理函数 foo
- 创建
script
标签,src
的属性为请求发送的目标地址,并且在最后加上callback=foo - 服务端在收到请求后,解析参数并计算返回数据,输出 foo(data) 字符串
- foo(data) 会放到script标签作为JS执行。此时会调用foo(data)作为参数
例:
function addScript(url){
var script = document.createElement('script'); // 创建一个 script 标签
script.src = url; // 将script标签的 src 属性指向目标地址。需要调用时就传入对应的 url
document.body.appendChild(script); // 添加这个标签以供加载
}
// 传入一个回调函数,并且使得回调函数的名字为 foo
addScript("http://localhost:8080/jsonp?callback=foo");
// 返回的是 foo(data) 字符串,被包含在script标签中 以JS的方式解析执行
// 因为存在下面这个同名函数, 所以上述JS的执行结果就是 函数foo 的执行结果
function foo(data){
console.log(data)
}
当上述代码的 data
是一个对象时,则可以调用对象的属性来输出更多内容。后台处理部分如下:
app.get('/jsonp',function(req,res) {
var data = {"jsonp": "succuess"};
var cb = req.query.callback;
if(cb)
res.send(cb+'('+JSON.stringify(data)+')');
else
res.send(data);
// foo({"jsonp": "succuess"})
})
而对于jQuery,跨域的实现方式是封装在了$.ajax()
中。但是由上述代码可知跨域与XMLHttpRequest
可以说并没有什么关系,但是解决了AJAX的“痛点” (两者本质上也是有区别的:ajax的核心是通过XMLHttpRequest获取非本页内容,而JSONP的核心则是动态添加<script>
标签来调用服务器提供的js脚本)。
下面是jQuery的实现方式:
$.ajax({
url: "http://localhost:8080/jsonp",
dataType: 'jsonp',
jsonp: "callback",
// jsonpCallback: "foo"
})
.done(function(res) {
console.log(res);
})
// 后台代码不变,仍输出 "jsonp": "succuess"
实际完整请求为:http://localhost:8080/jsonp?callback=foo&_=54681548735
,最后的随机字符串是jQuery添加的。这个请求的参数含义如下:
-
dataType
: 'jsonp',用于表示这是一个 JSONP 请求 -
jsonp
: 'callback',用于告知服务器根据这个参数获取回调函数的名称,通常约定就叫 callback。 -
jsonpCallback
: 'foo',回调函数的名称,即前面callback参数的值。(该参数可省略,jQuery 会自动生成一个随机字符串作为函数名)
注意: JSONP 存在安全隐患,动态插入<script>标签其实就是一种脚本注入(Dynamic script injection)。因为JavaScript没有任何权限与访问控制的概念,通过动态脚本注入的代码可以完全控制整个页面,所以引入外部来源的代码须多加小心。
2.2、CORS
CORS(Cross-origin resource sharing):跨域资源共享,是一个W3C标准。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
原理:服务器端对于CORS的支持,主要就是通过设置 Access-Control-Allow-Origin
来进行的。当浏览器检测到服务器设置 Access-Control-Allow-Origin
HTTP响应头之后,就会允许跨域请求。其中AJAX代码的相对路径要设置成其他域的绝对路径,也就是将要跨域访问的接口地址。
例:
document.querySelector("button").addEventListener("click", function(){
var xhr = new XMLHttpRequest(); // 新建AJAX请求
xhr.open("GET", "http://localhost:8080/cors"); // URL请求的是绝对路径
xhr.send();
xhr.onload = function(){
if(this.status == 200||this.status == 304)
console.log(xhr.responseText);
}
})
// 后端代码
app.get('/cors',function(req,res) {
var data = {"cors": "succuess"};
res.header("Access-Control-Allow-Origin", "*"); // 通配符 一切网站的请求都接受
//res.header("Access-Control-Allow-Origin", "http://localhost:8080"); 仅接受来自该域名的请求
res.send(data);
})
当设置header("Access-Control-Allow-Origin", "http://localhost:8080")
时,仅能允许访问来自localhost
且端口为8080的请求。即使是同IP127.0.0.1:8080
的请求 也不会允许访问。
2.3、window.postMessage
window.postMessage() 方法可以安全地实现跨源通信。该方法被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个 MessageEvent
消息。
window.postMessage(data,origin)方法接受两个参数:
- data:要传递的数据。
- origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写。postMessage()方法只会将message传递给指定窗口,也可以将参数设置为"*"以传递给任意窗口,如需指定和当前窗口同源则设置为"/"。
postMessage()是HTML5的一个API,使用这种方法最重要的就是发送消息和接受消息。在页面A中向页面B发送请求,然后在页面B中监听请求并获取A发送的数据,即可实现跨域。
- 页面A发送消息:调用postMessage API向目标窗口B 发送消息
window.postMessage(data, origin)
- 页面B接收消息:目标窗口B监听
message
事件window.addEventListener('message',function (e) { console.log(e.origin,e.data) })
例:页面A localhost:8080/index.htm
代码
<iframe src="http://127.0.0.1:8080/s/second.html"></iframe>
<script>
window.onload = function(){
// 向页面B发送数据“hello world” - postMessage
window.frames[0].postMessage("Hello World", "http://127.0.0.1:8080/s/second.html");
}
</script>
页面B http://127.0.0.1:8080/s/second.html
代码
<h1>I'm second level index</h1>
<script>
// 监听“消息”事件 来获取传递数据的内容(存在于event对象中)
window.addEventListener('message', function (event) {
var header = document.createElement("h2");
header.innerText = event.data; // 把收到的数据放入到新节点h2中
document.body.appendChild(header); // 插入节点以便显示
});
</script>

2.4、window.name
window.name: 在一个窗口(window)的生命周期内,窗口载入的所有页面共享一个 window.name
,每个页面对 window.name
都有读写权限, window.name
持久存在一个窗口载入过的所有页面中。
问题来源: 当我们设置一个iframe时,若不遵守同源策略,则无法获取其中的数据。当插入如下代码时,会因为违反同源策略而报错。
<iframe src="http://127.0.0.1:8080/s/second.html"></iframe>
<script>
window.onload = function(){
console.log( window.frames[0].contentWindow.name );
}

原理: 当 iframe
的页面跳到其他地址时,其 window.name
值保持不变(name值大小限制为2MB,不同浏览器限制大小也不同)。浏览器跨域 iframe
禁止互相调用/传值。但是调用 iframe
时 window.name
却不变,正是利用这个特性来互相传值。
解决方法:目标域的 window.name
存储需要的数值,然后生成 iframe的 src
属性先指向目标域。当把iframe的 src
属性改变时,它window.name属性值不变。即将这个 src
转向本地域某个代理时,跨域数据就由iframe的 window.name
从外域传递到本地域。具体步骤如下:
- 在应用页面A 中创建一个iframe,将其src指向数据页面B。数据页面B中的
window.name
存储着数据 - 在应用页面A中监听iframe的onload事件,在此事件中设置iframe的
src
属性指向本地域的代理文件proxy.html
(代理文件有自己创建,最好是空页面 方便加载) - 获取数据后,为保证安全,以免被其他域的iframe访问,要销毁这个iframe。

栗子:
// 页面A: http://localhost:8080/index.html 代码
window.onload = function () {
var state = 0; // 设置一个状态码 来标明iframe的src加载状态
var iframe = document.createElement("iframe");
iframe.src = 'http://127.0.0.1:8080/s/second.html'; // 生成一个iframe并将其src指向目标页B
// iframe的src改变后会重新加载一次, 所以onload事件会触发两次(两个if语句都会触发)
iframe.onload = function () {
if (state === 0) {
iframe.contentWindow.location = "http://localhost:8080/proxy.html"; // 将iframe页面的src指向代理页面 使得与页面A同源
state = 1; // 状态码变为1 即代表iframe已同源
}
if (state === 1){
console.log(iframe.contentWindow.name); // 获取数据
/*
// 获取数据后销毁iframe
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
*/
}
};
document.body.appendChild(iframe);
}
// 页面B: http://127.0.0.1:8080/s/second.html 代码
window.name = "window.name success!"; // 这个字符串可以换成任意内容,但最后都会被转换为String
使用这种方式,加载时 iframe 会闪现一下 然后消失。可以把这个 iframe 加载到对应的盒子中,并利用css对其进行渲染, 比如设置opacity: 0; height: 0; width: 0;
或display:none
来避免这种闪动带来的不良体验。
扩展阅读: 《iframe跨域通信的通用解决方案》
2.5、document.domain
document.doamin():获取/设置当前文档的原始域部分, 用于同源策略。
原理:浏览器中不同域的框架之无法进行JS交互。比如也面A: http://www.a.xxx.com:8080/s/second.html
,它里面有个iframe,src: http://www.b.xxx.com:8080/B/B.html
,因为页面与iframe框架属于不同域,所以无法通过JS来获取iframe中的东西
// 页面A : http://www.a.xxx.com:8080/s/second.html
<h1>Page A</h1>
<iframe src="http://127.0.0.1:8080/s/second.html"></iframe>
iframe.onload = function () {
var ifrDoc = iframe.contentDocument || iframe.contentWindow.document;
var h1 = ifrDoc.querySelector("h1").innerHTML;
console.log(h1);
ifrdoc.querySelector("h1").innerHTML = "h2"; // 获取Page B元素并修改内容
}
// 页面B: http://www.b.xxx.com:8080/B/B.html
<h1>Page B</h1>

但是不同框架间(父子或同辈),能够获取到彼此window对象。这时,document.domain就可以派上用场了。只要把 http://www.example.com/a.html 和 http://example.com/b.html 这两个页面的document.domain都设成相同的域名就可以了。
解决方法: 在相同主域名不同子域名下的页面,设置document.domain使它们同域。
注意:document.domain的设置是有限制的,只能把document.domain设置成自身或更高一级的父域,且主域必须相同。
// 页面A : http://www.a.xxx.com:8080/s/second.html
document.domain = "xxx.com";
// 页面B: http://www.b.xxx.com:8080/B/B.html
document.domain = "xxx.com";

如果仅想获取信息而不展示页面,并且不想再HTML中添加无意义的标签。可以对页面A的JS代码进行如下设置:
// 页面A : http://www.a.xxx.com:8080/s/second.html
document.domain = "xxx.com"
var iframe = document.createElement("iframe");
iframe.src = "http://www.b.xxx.com:8080/B/B.html";
iframe.onload = function () {
var ifrdoc = iframe.contentDocument || iframe.contentWindow.document;
var h1 = ifrdoc.querySelector("h1").innerHTML;
console.log(h1);
}
iframe.style.display = "none"; // 取消iframe的展示
document.body.appendChild(iframe);
3、几种跨域方式的比较
跨域方式 | 优点 | 缺点 |
---|---|---|
JSONP | 兼容性好;简单适用,服务器改造小 | 只能用于GET方法;有一定安全隐患;没有关于 JSONP 调用的错误处理(比如脚本内容错误提示,无法获取404) |
CORS | 任何HTTP请求均可;前端请求简单方便 | 兼容性稍差(IE10以上) |
window.postMessage | 无需后端配合;移动端兼容性好 | 浏览器需要支持HTML5,获取窗口句柄后才能相互通信 |
window.name | 无需前后端配置;兼容性好 | 传递的数据仅限于字符串(对象或其他会自动转化为字符串);需要再添一个代理页面;需要额外加载两个iframe |
document.domain | 所有浏览器都支持 | 需要主域相同且子域不同;只适用于父子window间通信,不能用于XMLHttpRequest; |
CORS与JSONP对比:
- JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。
- 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。
- JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS)。
JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
网友评论