跨域
- 什么是跨域
- 跨域方法
- JSONP
- CORS
- window.postMessage
- window.name
- document.domain
- 跨域方式间比较
什么是跨域
概念:只要不遵循同源策略的请求,都被当作是跨域的。
同源策略( 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 |
不同域名 | × |
对于端口和协议的不同,只能通过后台来解决。前端针对不同域名,则有以下几种解决方案。
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没有任何权限与访问控制的概念,通过动态脚本注入的代码可以完全控制整个页面,所以引入外部来源的代码须多加小心。
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的请求。即使是同IP 127.0.0.1:8080
的请求 也不会允许访问。
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>

window.postMessage result
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 ); }

Error message
原理: 当 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。

window.name solution
栗子:
// 页面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跨域通信的通用解决方案》
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>

Error message of document.domain
但是不同框架间(父子或同辈),能够获取到彼此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";

set document.domain
如果仅想获取信息而不展示页面,并且不想再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);
几种跨域方式的比较
跨域方式 | 优点 | 缺点 |
---|---|---|
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的网站请求数据。
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。