一、什么是跨域
跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。
| 页面地址 | 被请求的url | 是否跨域 | 原因 |
|---|---|---|---|
| www.abc.com | www.abc.com/index.html | 否 | 同源(协议、域名、端口号相同) |
| www.abc.com | www.abc.com/index.html | 是 | 协议不同(http/https) |
| www.abc.com | www.qwe.com | 是 | 主域名不同(test/qwe) |
| www.abc.com | yuque.abc.com | 是 | 子域名不同(www/yuque) |
| www.abc.com:8080/ | www.abc.com:8888/ | 是 | 端口号不同(8080/8888) |
同源策略限制了浏览器的行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS 对象无法获取
- Ajax请求发送不出去
二、为什么会跨域
出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。
三、跨域解决办法
1,jsonp
- JSONP是一个非官方协议,它允许在服务器端集成script tags返回至客户端,通过javascript callback的形式实现跨域访问。
- 基本思想:网页通过添加一个
<script type="text/javascript">
function jsonpCallback(result){
//alert(result);
for(var i in result){
alert(i + ":" + result[i]); //循环输出
}
}
var JSONP = document.createElement("script");
JSONP.type = "text/javascript";
JSONP.src = "http://crossdomain.com/services.php?callback=jsonpCallback";
document.getElementsByTagName("head")[0].appendChild(JSONP);
</script>
jsonp缺点:只能实现get一种请求。
2,document.domain + iframe跨域
此方案仅限主域相同,子域不同的跨域应用场景。 实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域
- 父窗口:(www.a.com/a.html)
<iframe id="iframe" src="http://child.a.com/b.html"></iframe>
<script>
document.domain = 'a.com';
var user = 'admin';
</script>
- 子窗口:(child.a.com/b.html)
<script>
document.domain = 'a.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
3,location.hash + iframe跨域
实现原理: a与b跨域相互通信,通过中间页c来实现(且c与a是同域)。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。 具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
- a.html:(www.a.com/a.html)
<iframe id="iframe" src="http://www.b.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:(www.b.com/b.html)
<iframe id="iframe" src="http://www.a.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>
- c.html:(www.a.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
4, window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
- a.html:(www.a.com/a.html)
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(同域c页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.a.com/c.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.b.com/b.html', function(data){
alert(data);
});
- c.html:(www.a.com/c.html) 中间代理页,与a.html同域,内容为空即可。
location = 'http://parent.url.com/xxx.html';
- b.html:(www.b.com/b.html)
<script>
window.name = 'This is b.html data!';
</script>
这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。
5, postMessage跨域
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数 data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。 origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
- a.html:(www.a.com/a.html)
<iframe id="iframe" src="http://www.b.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.b.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from b ---> ' + e.data);
}, false);
</script>
- b.html:(www.b.com/b.html)
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from a ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.a.com');
}
}, false);
</script>
6, 跨域资源共享(CORS)
CORS 通信过程都是浏览器自动完成,需要浏览器(都支持)和服务器都支持,所以关键在只要服务器支持,就可以跨域通信,CORS请求分两类,简单请求和非简单请求 另外CORS请求默认不包含Cookie以及HTTP认证信息,如果需要包含Cookie,需要满足几个条件:
- 服务器指定了 Access-Control-Allow-Credentials: true
- 开发者须在请求中打开withCredentials属性: xhr.withCredentials = true
- Access-Control-Allow-Origin不要设为星号,指定明确的与请求网页一致的域名,这样就不会把其他域名的Cookie上传
简单请求 需要同时满足两个条件,就属于简单请求:
- 请求方法是:HEAD、GET、POST,三者之一
- 请求头信息不超过以下几个字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-Id
- Content-Type:值为三者之一application/x-www/form/urlencoded、multipart/form-data、text/plain。 application/x-www-form-urlencoded: 表示使用URL编码的方式来编码表单。如果没有将enctype属性设置为任何值,那么这就是默认值。multipart/form-data: 当用户想上传文件这种二进制等文件或者前面的那个方式不能满足时,使用这种类型的表单。text/plain: 文本形式,只发送数据而不进行任何编码时使用
凡是不同时满足上面两个条件,就属于非简单请求。 浏览器对这两种请求的处理,是不一样的。
需要这些条件是为了兼容表单,因为历史上表单一直可以跨域。 浏览器直接发出CORS请求,具体来说就是在头信息中增加Origin字段,表示请求来源来自哪个域(协议+域名+端口),服务器根据这个值决定是否同意请求。如果同意,返回的响应会多出以下响应头信息。
Access-Control-Allow-Origin: http://juejin.com // 和 Orign 一致 这个字段是必须的
Access-Control-Allow-Credentials: true // 表示是否允许发送 Cookie 这个字段是可选的
Access-Control-Expose-Headers: FooBar // 指定返回其他字段的值 这个字段是可选的
Content-Type: text/html; charset=utf-8 // 表示文档类型
在简单请求中服务器至少需要设置:Access-Control-Allow-Origin 字段
非简单请求 比如 PUT 或 DELETE 请求,或 Content-Type 为 application/json ,就是非简单请求。 非简单 CORS 请求,正式请求前会发一次 OPTIONS 类型的查询请求,称为预检请求,询问服务器是否支持网页所在域名的请求,以及可以使用哪些头信息字段。只有收到肯定的答复,才会发起正式XMLHttpRequest请求,否则报错。 预检请求的方法是OPTIONS,它的头信息中有几个字段。
- Origin: 表示请求来自哪个域,这个字段是必须的。
- Access-Control-Request-Method:列出CORS请求会用到哪些HTTP方法,这个字段是必须的。
- Access-Control-Request-Headers: 指定CORS请求会额外发送的头信息字段,用逗号隔开。
- OPTIONS请求次数过多也会损耗性能,所以要尽量减少OPTIONS请求,可以让服务器在请求返回头部添加。
Access-Control-Max-Age: Number // 数字 单位是秒
表示预检请求的返回结果可以被缓存多久,在这个时间范围内再请求就不需要预检了。不过这个缓存只对完全一样的URL才会生效。
7, Nginx与Node
在工作上,由于工作平台和语言的原因,对于大部分前端开发人员来说,更倾向于用Nodejs来搭建服务器,进而实现一些需求,对Nginx有天然的抗拒感。的确,Nginx中的绝大部分功能,如果单纯的使用Node.js也可以满足和实现。但实际上,Nginx和Node.js并不冲突,都有自己擅长的领域:Nginx更擅长于底层服务器端资源的处理(静态资源处理转发、反向代理,负载均衡等),Node.js更擅长于上层具体业务逻辑的处理。两者可以实现完美组合,助力前端开发。
- 正向代理
翻墙工具其实就是一个正向代理工具。它会把 们访问墙外服务器server的网页请求,代理到一个可以访问该网站的代理服务器proxy,这个代理服务器proxy把墙外服务器server上的网页内容获取,再转发给客户。
- 反向代理
客户端发送的请求,想要访问server服务器上的内容。但将被发送到一个代理服务器proxy,这个代理服务器将把请求代理到和自己属于同一个LAN下的内部服务器上,而用户真正想获得的内容就储存在这些内部服务器上。 这里proxy服务器代理的并不是客户,而是服务器,即向外部客户端提供了一个统一的代理入口,客户端的请求,都先经过这个proxy服务器,至于在内网真正访问哪台服务器内容,由这个proxy去控制。
使用反向代理最主要的两个原因: (1)安全及权限。可以看出,使用反向代理后,用户端将无法直接通过请求访问真正的内容服务器,而必须首先通过Nginx。可以通过在Nginx层上将危险或者没有权限的请求内容过滤掉,从而保证了服务器的安全。 (2)负载均衡。单个服务器解决不了,我们增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为请求分发到多个服务器上,将负载分发到不同的服务器,也就是我们所说的负载均衡。
在vue.config.js文件中,我们需要配置
devServer: {
//代理列表
proxy: {
'/api': {
target: 'http://192.10.24.81:8088', //要代理的域名
changeOrigin: true,//允许跨域
pathRewrite: {
'^/api': '' // 这个是定义要访问的路径
}
}
}
}
/api/getUserMsg 相当于 http://192.10.24.81:8088/getUserMsg 某个项目时,由于是多个后端配合(A写一般任务的接口,B写技术预研的接口),出现了多个端口。因此,前端也需要配置多个代理。
devServer: {
//代理列表
proxy: {
'/api_a': {
target:'http://xxxxxx:7890',//线上a
changeOrigin: true,
pathRewrite: {
'^/api_a': '/api_a'
}
},
'/api_b': {
target:'http://xxxxx:7777',//线上b
changeOrigin: true,
pathRewrite: {
'^/api_b': '/api_b'
}
},
'/api_c': {
target:'http://xxxxxx:8090',//线上c
changeOrigin: true,
pathRewrite: {
'^/api_c': '/api_c'
}
}
}
}
8, WebSocket协议跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。 原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
// 使用 socket.io 插件
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
const socket = io('http://xxxx:8080'); // 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('新消息' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('连接关闭');
})
})
</script>
四、总结
- CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
- JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
- 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
- 日常工作中,用得比较多的跨域方案是cors和nginx反向代理