浏览器在请求不同域的资源时,会因为同源策略的影响请求不成功,这就是通常被提到的“跨域问题”。作为前端开发,解决跨域问题应该是一个被熟练掌握的技能。而随着技术不断的更迭,针对跨域问题的解决也衍生出了多种解决方案。我们通常会根据项目的不同需要,而采取不同的方式。这篇文章,将详细总结跨域问题的相关知识点,以便在遇到相同问题的时候,能有一个清晰的解决思路。
跨域问题的产生
跨域是由浏览器同源策略限制的一类请求场景。
同源策略(SOP)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
Cookie、LocalStorage 和 IndexDB 无法读取
DOM 和 JS对象无法获得
AJAX 请求不能发送
常见的跨域场景
解决跨域的方法
通过jsonp跨域
document.domain + iframe跨域
location.hash + iframe
window.name + iframe跨域
postMessage跨域
跨域资源共享(CORS)
nginx代理跨域
nodejs中间件代理跨域
WebSocket协议跨域
这里挑选几个简单常见的前端跨域方案来介绍
通过jsonp跨域
原理:动态创建script标签,利用script的src不受同源策略约束来跨域获取数据。
优点:对于浏览器兼容性良好;
缺点:只能用于GET请求,并且需要后端配合对接口做一定的改变。
原生js实现jsonp
//回调函数
function showData(data) {
for (let i in data) {
console.log(i + ':' + data[i]);
}
}
function getInfo() {
const script = document.createElement('script');
const url = 'http://xxxx/xx.php?callback=showData';
script.type = 'text/javascript';
script.setAttribute('src', url);
document.head.appendChild(script);
}
//需要时调用
getInfo();
当执行getInfo()时,就发送http://xxxx/xx.php?callback=showData请求,正常请求该接口时,服务端会直接返回数据,如;而当用jsonp时,服务端会接受这个callback参数,然后用这个参数值包装要返回的数据,如showData(),并且该函数会被立即执行。
jQuery实现jsonp
$.ajax({
url: 'http://xxx/xx?callback=?', //不指定回调名,可省略callback参数,会由jQuery自动生成
dataType: 'jsonp',
jsonpCallback: 'demo', //用于添加自己的回调函数,无此项则回调函数默认为success
success: function(data) {
console.log(data.msg);
}
});
利用$.ajax方法,只要指定dataType为jsonp即可。
document.domain + iframe跨域
主要用于主域相同,子域不同的页面之间进行交互。
前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域。
原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
document.domain = 'blog.com';
var user = 'index';
document.domain = 'blog.com';
// 获取父窗口中变量
如果两个页面不设置document.domain,则a页面中可以看到b页面的内容,却不能通过js来操作它。因为这两个页面主域相同,子域不同,在操作之前,js会检测两个页面的域是否相等,如果相等,就允许其操作,如果不相等,就会拒绝操作。
我们通过设置document.domain,将两个页面的domain设置成一致,这样就可以在两个页面之间互相操作了。
location.hash + iframe
原理: a.html欲与b.html跨域相互通信,通过中间页c.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
优点:
可以解决域名完全不同的跨域;
可以实现双向通讯。
缺点:
location.hash会直接暴露在URL里,并且在一些浏览器里会产生历史记录,数据安全性不高也影响用户体验;
由于URL大小的限制,支持传递的数据量不大;
需新增一个代理文件来实现数据传递。
举个栗子:
页面:
first.com/first.html
first.com/cross.html
second.com/second.html
实现:first.html与second.html的交互
思路:first.html-->second.html-->cross.html-->first.html
first.html通过iframe引入second.html;
second.html通过iframe引入cross.html;
cross.html与first.html同域,可直接通过js访问来通信。
var iframe = document.getElementById('iframe');
function onCallback(hash) {
if (hash === '#red') {
document.getElementById('btn').style.color = 'red';
}
}
function changeColor() {
iframe.src = iframe.src + '#red';
}
var iframe = document.getElementById('iframe');
window.onhashchange = function () {
const hash = location.hash;
iframe.src = iframe.src + hash;
};
window.onhashchange = function () {
};
在first.html中,我们定义一个回调函数onCallback(),该函作用是将按钮的颜色改成红色,当我们点击按钮时,执行changeColor()事件,将hash值传给second.html;
在second.html中,绑定onhashchange事件,当hash值改变时,因为second.html和first.html不同域,此时直接执行first.html中的onCallback()事件,会提示跨域操作的错误信息,所以我们将该hash值传给cross.html;
在cross.html中,绑定onhashchange事件,当hash值改变时,因为cross.html和first.html同域,所以可以执行first.html中的onCallback()事件。
这样就实现了first.html和second.html之间的通信。
window.name + iframe跨域
原理:window对象有个name属性,该属性有个特征,即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置,并且可以支持非常长的name值(2MB)。
比如有个http://www.a.com/a.html页面,需要通过a.html页面里的js来获取另一个位于不同域上的页面www.b.com/b.html里的数据。
a.html(http://www.a.com/a.html)
function getData() {
var iframe = document.getElementById('iframe');
iframe.onload = function () {
//此时iframe和a.html处于同一域中,可以互相访问
var data = iframe.contentWindow.name;
console.log(data);
};
//about:blank,javascript: 和 data: 中的内容,继承了载入他们的页面的源,此处也可以设置成www.a.com下的任意页面。
iframe.src = 'about:blank';
}
b.html(http://www.b.com/b.html)
window.name = '';
a.html通过iframe引入b.html,当iframe加载完成时执行getData()函数,因为a.html和b.html是不同域,所以a.html不能直接获取iframe中的name值,而getData()函数的作用就是将iframe和a.html设置成同一域,使得a.html能够访问到iframe中的数据。
上述代码只是演示window.name如何用于跨域操作,实际应用中我们可以对该过程进行封装,比如动态创建iframe等,为了安全,获取完数据后,还需销毁该iframe,此处不再示范。
postMessage跨域
在 HTML5 中, window 对象增加了一个非常有用的方法:
windowObj.postMessage(message,targetOrigin)
windowObj:接受消息的Window对象。
message:在最新的浏览器中可以是对象。
targetOrigin:目标的源,*表示任意。
可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。
比如有两个页面:www.a.com/a.html和www.b.com/b.html,用window.postMessage来实现两个页面之间相互通信。
a.html(www.a.com/a.html)
var iframe = document.getElementById('iframe');
iframe.onload = function () {
var data = {
name: 'eric',
age: 27,
sex: 'man'
};
// 向b.html传送跨域数
iframe.contentWindow.postMessage(JSON.stringify(data),'http://www.b.com');
};
// 接受b.html返回数据
window.addEventListener('message', function (e) {
alert('data from b.html ---> ' + e.data);
});
b.html(www.b.com/b.html)
// 接收a.html的数据
window.addEventListener('message', function (e) {
alert('data from a.html ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回a.html
}
});
a.html页面加载完成时,向b.html传送跨域数据,b.html接收到该数据后,进行了一定处理后又返回给a.html。
其中message事件就是用来接收postMessage发送过来的请求的。函数参数的属性有以下几个:
origin:发送消息的window的源;
data:数据;
source:发送消息的Window对象。
跨域资源共享(CORS)
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
详细解析可以参考阮一峰的文章:跨域资源共享 CORS 详解(http://www.ruanyifeng.com/blog/2016/04/cors.html),主要设置在服务端,这里简单举个例子来演示:
前端设置:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.b.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
Java后台:
/*
* 接口参数中定义:HttpServletResponse response
*/
response.setHeader("Access-Control-Allow-Origin", "http://www.a.com"); // 若有端口需写全(协议+域名+端口)
response.setHeader("Access-Control-Allow-Credentials", "true");
PHP后台:
// 设置允许其他域名访问
header('Access-Control-Allow-Origin:*');
// 设置允许的响应类型
header('Access-Control-Allow-Methods:POST, GET');
// 设置允许的响应头
header('Access-Control-Allow-Headers:x-requested-with,content-type');
NodeJs后台:
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function (req, res) {
var postData = ' ';
// 数据块接收中
req.addListener('data', function (chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function () {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.a.com', // 允许访问的域(协议+域名+端口)
'Set-Cookie': 'l=a123456;Path=/;Domain=www.b.com;HttpOnly' // HttpOnly:脚本无法读取cookie
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen('8080');
console.log('Server is running at port 8080...');
这里推荐参考张鑫旭写的一个案例:CORS ajax跨域请求php简单完整案例一则(http://www.zhangxinxu.com/wordpress/2018/02/cors-ajax-xmlhttprequest-php/)来辅助学习。
------------笑对人生,能穿透迷雾;笑对人生,能坚持到底;笑对人生,能化解危机;笑对人生,能照亮黑暗。
领取专属 10元无门槛券
私享最新 技术干货