跨域是什么?
跨域是 允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
为什么有跨域:
出于安全原因,浏览器有同源限制,限制从脚本内发起跨源的HTTP请求,
例如: XMLHttpRequest和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源, 除非响应报文包含了正确CORS响应头。[解决跨域]
什么是XMLHttpRequest:
XMLHttpRequest是一个浏览器接口,使得Javascript可以进行HTTP(S)通信。 最早,微软在IE 5引进了这个接口。因为它太有用,其他浏览器也模仿部署了,ajax操作因此得以诞生。 但是,这个接口一直没有标准化,每家浏览器的实现或多或少有点不同。 HTML 5的概念形成后,W3C开始考虑标准化这个接口。 2008年2月,就提出了XMLHttpRequest Level 2 草案。 这个XMLHttpRequest的新版本,提出了很多有用的新功能,将大大推动互联网革新。 参考: XMLHttpRequest Level 2 使用指南
什么是AJAX :
AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。 AJAX 不是新的编程语言,而是一种使用现有标准的新方法。 AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下。
什么是同源策略:
具体可以看浏览器同源政策及其规避方法 的这边文章 写的很好,想简单了解下的可以看下面的
1.1 定义:
- 协议
- 域名
- 端口
一致才叫同源
1.2 目的:
是为了保证用户信息的安全,防止恶意的网站窃取数据。
1.3 限制范围,目前三种行为受限制
1. Cookie、LocalStorage 和 IndexDB 无法读取。
【单点登录的同源实现方式就是通过设置cookie的domain】
- cookie: 只有同源网页才能共享
- 一级域名相同,二级不相同,可以设置document.domain
- 服务器指定所属cookie的一级域名
Set-Cookie: key=value; domain=.example.com; path=/- 这样的话二级三级都可以读取这个cookie
- LocalStorage:
- window.postMessage ( HTML5 引入的新的API ) : 允许窗口通信
- 子父窗口都可以通过监听message事件
- indexDB: 同上
2. DOM 无法获得。
- iframe: 无法与父窗口通信
- 一级域名相同,二级不同,可以设置document.domain
- 完全不同源
- 片段标识符 #
- url #后的部分,改变片段标识符,页面不会刷新
- 父窗口可以把信息写入子窗口的片段标识符
- 子窗口监听hashchange事件
- url #后的部分,改变片段标识符,页面不会刷新
- window.name
- 缺点:必须监听子窗口的window.name ,影响性能
- 优点:容量很大,可以防止很长的字符串
- 跨文档通信API window.postMessage
- 片段标识符 #
- window.open: 同上
3. AJAX 请求不能发送。
- jsonp:JSONP跨域原理分析
- CORS: CORS(cross-origin resource sharing)跨域资源共享
- websocket:
解决跨域的几种主要方法
一、AJAX(核心)
1. JSONP跨域原理分析
思路:
利用scriptd 的src不受同源策略限制,动态生成script。和后端约定相同的函数名,服务端返回的数据包裹在函数名里。缺点是只支持get。
2. CORS(cross-origin resource sharing)跨域资源共享
大概思路:
- 如果是简单请求比如get,post,head 等浏览器会在请求头里自动加一个origin,服务端需要返回Access-Control-Allow-Origin 字段 , 代表允许所有跨域,也可以是指定域名。*
- 如果是非简单请求比如说put、delete、content-type: application/json 会发出预检请求options,在正式请求前先问问服务器当前网页是否在名单内,是的话发出正式请求,不是的话报错,有有效期字段 代表多久内不需要发options请求
总之:需要服务端的配合
3. websocket 跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
服务端推送技术还有HTML5新增的WEB API :SSE单向事实服务端推送和 【HTTP2的服务端推送预资源机制】
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
二、 服务器代理:浏览器请求同源服务器,再由后者请求外部服务
同源策略仅是针对浏览器的安全策略。 服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。 实现思路: 通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。
1. nginx 代理跨域
location / {
add_header Access-Control-Allow-Origin *;
}
#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
2. nodejs 中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同。 都是通过启一个代理服务器,实现数据的转发。 也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
- 非VUE: 使用node + express + http-proxy-middleware搭建一个proxy服务器。
# 前端代码
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
# 中间件服务器代码:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
- VUE:node + vue + webpack + webpack-dev-server
# webpack.config.js部分配置:
# 开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
三、窗口
1. 设置document.domain :一级相同,二级不同的情况下
此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
# 父窗口
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
-------------------------------
# 子窗口
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
2. hash路由 片段标识符(#)
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
# a.html
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
# b.html
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
# domain1.com/c.html
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
3. window.name:
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
4. window.postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数:
- data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
- origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
a.html:(www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
# 同样监听message 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
b.html:(www.domain2.com/b.html)
# 监听message 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
重点: 1向2传递 : iframe.contentWindow.postMessage 2向1返回:window.parent.postMessage 两边都监听message 接受收到的数据
总结:
- ajax相关的有三个,最常用的是CORS,JSONP只支持get,都需要服务端配合
- 窗口有关的有4个,postMessage 最全面,domain的设置需要主域一样,name性能不好
- 服务器代理相关的原理都是一样的,比较适合前后端分离的项目,需要有一定的经验
没有最好, 只有适合