同源策略
同一协议、同一域名、同一端口号,如果这三个条件中有任何一条不满足,就不允许两个脚本进行交互。
跨域
定义:跨域,指的是浏览器不能执行其他网站的脚本。是由浏览器的同源策略造成的,是浏览器对js施加的安全限制。只要协议(http/https)、域名、端口有任何一个不同,都被当作是不同的域。
什么情况下会出现跨域
工程服务化后,不同职责的服务分散在不同的工程中,往往这些工程的域名是不同的,但一个需求可能需要对应到多个服务,这时便需要调用不同服务的接口,因此会出现跨域。
解决跨域的方案
JSONP
定义:
通过<script>标签引入一个js文件,这个js文件加载成功后会执行url参数中指定的函数,并且会把json数据作为参数传入。
⚠️:jsonp是需要服务器端配合的
⚠️:jsonp支持get请求,不支持post请求
前端:前端设置好回调函数,并把回调函数当做请求url的参数.
<script>
function getPrice(data){
console.log(data);
}
</script>
<script
type="text/javascript"
src="http://sdffw.b2b.com/getSupplyPrice?callback=getPrice&bcid=47296567">
</script>
后端: 服务端不再返回的是一个JSON格式的数据,而是返回一段JS代码,将JSON的数据以参数的形式传递到这个函数中,而函数的名称就是callback参数的值。
const url = require('url');
require('http').createServer((req, res) => {
const data = {};
const callback = url.parse(req.url, true).query.callback ;
res.writeHead(200)
res.end(`${callback}(${JSON.stringify(data)})`)
*// 服务器收到请求后,解析参数,*
*// 将callback(data)以字符串的形式返还数据,前端页面会将callback(data)作为js执行*
*// 调用jsonpCallback(data)函数。*
}).listen(3000, '127.0.0.1');
callback是前后台约定的查询参数,服务器端返回一个能执行的js文件,这个js文件是调用callback对应的参数值即getPrice执行,并且返回对应的数据,我们可以在getPrice方法里面来处理返回的数据,最终返回的结果如下:
getPrice({
"data":{"priceType":"0","unit":"斤"},
"message":"价格获取成功!!!",
"state":"1"
})
原理:
- 声明一个全局回调函数,参数为服务端返回的 data。
- 创建一个 script 标签,拼接整个请求 api 的地址(要传入回调函数名称如 ?callback=getInfo ),赋值给 script 的 src 属性
- 服务端接受到请求后处理数据,然后将函数名和需要返回的数据拼接成字符串,拼装完成是执行函数的形式。(getInfo('server data'))
- 浏览器接收到服务端的返回结果,调用了声明的回调函数。
封装一个JSONP:
function myJsonp(url,data,callback){
var fnName = 'myJsonp_' + Math.random().toString().replace('.','');
//定义一个全局回调函数
window[fnName] = callback;
//初始化序列化参数
var querystring = '';
for(var attr in data){
querystring += attr + '=' + data[attr] + '&';
}
// 动态创建script标签
var script = document.createElement('script');
//后台接受回调函数,并调用
script.src = url + '?' + querystring + 'callback=' + fnName;
//处理完毕之后,删除script标签,否则多次请求,页面会存在多个script标签
script.onload = function(){
document.body.removeChild(script);
}
document.body.appendChild(script);
}
CORS(跨域资源共享)Cross-origin resource sharing
CORS的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。
这种方式的关键是后端进行设置,即是后端开启 Access-Control-Allow-Origin 为*或对应的 origin就可以实现跨域。
⚠️:CORS需要浏览器和服务器同时支持
⚠️:通常使用CORS时,浏览器会将异步请求分为简单请求和特殊请求两种
【简单请求】
只要同时满足以下两大条件,就属于简单请求:
(1) 请求方法是以下三种方法之一:
- GET
- HEAD
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
当浏览器发现发现的ajax请求是简单请求时,会在请求头中携带一个字段:Origin。
请求报文:
解析:Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。
如果服务器允许跨域,需要在返回的响应头中携带下面信息:
响应报文:
- Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意
- Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true。
⚠️:如果跨域请求要想操作cookie,需要满足3个条件:
- 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
- 浏览器发起ajax需要指定withCredentials 为true
- 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名
【特殊请求】
不符合简单请求的条件,会被浏览器判定为特殊请求。
例如:
1:使用了下面任一 HTTP 方法:(put、delete、connect、options、patch、trace)
2:人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
Accept、Accept-Language、Content-Language、Content-Type (but note the additional requirements below)、DPR、Downlink、Save-Data、Viewport-Width、Width
3:Content-Type 的值不属于下列之一:
application/x-www-form-urlencoded、multipart/form-data、text/plain
⚠️:特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
一个“预检”请求的样板:
与简单请求相比,除了Origin以外,多了两个头:
- Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
- Access-Control-Request-Headers:会额外用到的头信息
服务的收到预检请求,如果许可跨域,会发出响应:
除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:
- Access-Control-Allow-Methods:允许访问的方式
- Access-Control-Allow-Headers:允许携带的头
- Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了
如果浏览器得到上述响应,则认定为可以跨域。
postMessage
postMessage是HTML5 XMLHttpRequest Level2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
-
页面和其打开的新窗口的数据传递
-
多窗口之间消息传递
-
页面与嵌套的iframe消息传递
-
上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
- data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
- origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
1.)a.html:(www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;" onload="onload()"></iframe>
<script>
function onload() {
var iframe = document.getElementById('iframe');
var win = iframe.contentWindow; // 获取window对象
win.postMessage('传值必须是字符串', '*') // 向不同域发送消息
};
</script>
2.)b.html:(www.domain2.com/b.html)
<script>
// 接收domain1的数据
window.onmessage = function(e) { // 注册message事件接收消息
e = e || event; // 获取事件对象
alert(e.data); //通过date属性得到传送的消息
},;
</script>
Nginx代理跨域
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
nginx具体配置:
#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;
}
}
- 前端代码示例:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
WebSocket协议跨域
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
- 前端代码:
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
2)Nodejs socket后台:
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
Nodejs中间件代理跨域
通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。