1. 引入
1.1 同源策略
同源策略是一种约定,是浏览器最基本且核心地安全功能。所谓同源是指协议+域名+端口三者相同,即使两个不同的域名指向同一个ip地址,也非同源。
示例,现有一URL:
http://store.company.com/dir/page.html
//同源 因为只有路径不同
http://store.company.com/dir2/other.html
//同源 因为只有路径不同
http://store.company.com/dir/inner/another.html
//失败 因为协议不同(这里是https)
https://store.company.com/secure.html
//失败 因为端口不同(http默认端口是80)
http://store.company.com:81/dir/etc.html
//失败 因为域名不同
http://news.company.com/dir/other.html
同源策略限制了以下内容:
- Cookie,LocalStorage,IndexedDB等存储不能读取
- DOM节点无法获得
- AJAX请求会被浏览器拦截
这三个标签允许跨域加载资源(省略了前面的< 不然md格式显示不出来)
img src=xxx>
link href=xxx>
script src=xxx>
1.2 常见跨域场景
不同源之间互相请求资源,就算做“跨域”,跨域的目的就是为了阻止用户读取到不同域下的内容。
如果是协议和端口造成的跨域问题,前端是无能为力的。
要注意的是,跨域并不是请求发不出去,而是请求发出去了,服务端接收到请求且返回结果了,但是结果被浏览器拦截了
2.跨域解决方案
2.1 JSONP
2.1.1 概念和特点
JSONP是JSON的一种使用模式,利用script标签没有跨域限制的漏洞,网页可以得到其它源动态产生的JSON数据,但是JSONP一定要对方的服务器做支持才可以。
JSONP和AJAX相同点在于,都是从客户端向服务端发送请求,再从服务端获取数据。但AJAX属于同源策略,JSONP属于跨域请求。
JSONP的优缺点如下:
优点是简单且兼容性好,可用于解决主流浏览器的跨域数据访问的问题。
缺点是只支持get方法,导致可能会遭受XSS攻击。
2.1.2 实现流程
(1) 前端定义一个回调函数
(2) 在script的url中通过参数将函数传给后端
(3) 后端获取到前端传过来的函数,带上要回传的参数执行该函数
(4) 前端在script标签返回资源时就会去执行回调函数并拿到数据
先展示一个最基础的例子:
虽然两个文件端口号不同,但是还是成功访问到了程序B中的message,弹出success,体现了script标签的src属性不受同源策略约束。
程序A
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
//回调函数
function jsonp(data) {
alert(data.message);
}
</script>
程序B
//调用callback函数,并以json数据形式作为阐述传递,完成回调
jsonp({message:"success"});
像上面这个例子,对应着上面的实现流程来看:
(1) 定义好了jsonp这个回调函数,执行到第二个script时由于通过url请求了这个地址,那么后端就能拿到url里的参数
(2) 后端拿到函数参数也就是callback=jsonp后,就知道了前端想通过这个回调函数接收数据,因此将要回传的数据作为回调函数的参数并执行
(3) 前端得到了后端返回的内容jsonp({message:"success"})发现这是一个函数,于是执行该函数并弹出message
2.1.3 封装jsonp
一般情况下,我们希望这个script标签能够动态的调用,而不是像上面因为固定在html里面,没等页面显示就执行了,很不灵活,同时还可能遇到请求的回调函数名相同的情况。
我们可以通过javascript动态的创建script标签,这样我们就可以灵活调用远程服务了。
因此我们可以自己封装一个jsonp,然后传入相应的参数来执行:
function myJsonp(url, jsonpCallback, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
window[jsonpCallback] = function (data) {
success && success(data)
}
document.body.appendChild(script)
}
myJsonp('http://xxx', 'callback', function (value) {
console.log(value)
})
举个实际中动态创建script的例子,比如谷歌的ajax搜索接口:
<script>
//添加<script>标签的方法
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type", "text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
//搜索apple,将自定义的回调函数名result传入callback参数中
addScriptTag("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result");
}
//自定义的回调函数result
function result(data) {
//我们就简单的获取apple搜索结果的第一条记录中url数据
alert(data.responseData.results[0].unescapedUrl);
}
</script>
2.2 CORS
cors主要由后端支持。
浏览器会自动进行CORS通信,实现CORS通信的关键是后端。只要后端实现了CORS,就实现了跨域。
服务端设置Access-Control-Allow-Origin就可以开启 CORS。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
2.2.1 简单请求和复杂请求
虽然设置CORS和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求需要同时满足若干个条件:
(1)使用下列方法之一:
- GET
- HEAD
- POST
(2)Content-Type的值为下列之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
(3)请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequestUpload对象可以使用XMLHttpRequest.upload属性访问。
等等
满足浏览器的简单请求会直接发送,并在请求头中携带Origin表示本次来自哪个源。
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
axios.get("http://127.0.0.1:8080/api/corslist");
</script>
不符合上面条件的就是复杂请求。
会在发送请求之前发送一个预检请求,方法为OPTIONS,只包含头部信息,不包含请求体,单独一个包发给服务器。包含如下字段:
预检请求就是试探一下服务器那边是否允许自己发请求。
如果被拒绝了就不请求了,节省资源。
收到服务器响应后,分析它是否支持跨域,如果支持跨域则发送复杂请求,如果不支持则报错。
这里我们加入了一个非集合内的header头cc来达到实现复杂请求的目的。
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
axios.get("http://127.0.0.1:8080/api/corslist", { header: { cc: "xxx" } });
</script>
2.2.2 关于cors的cookie问题
想要传递cookie需要满足三个条件:
(1) 请求头设置withCredentials
(2) 响应头设置Access-Control-Allow-Credentials为true
(3) 响应头设置Access-Control-Allow-Origin为非*
2.3 websocket
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。
原生WebSocket API使用起来不太方便,我们使用'socket.io',它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
2.4 postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
2.5 Node正向代理
代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。
- 接收客户端请求
- 将请求转发给服务器
- 拿到服务器返回的数据
- 将数据转发给客户端
这里有很多种,比如自己项目里用到的proxy代理,具体的应用到时候写项目博客的时候讲解一下。
2.6 nginx反向代理
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。也就是自己搭建的中转服务器先向需要包含数据的服务器获取数据,再将获取到的数据传回给自己的客户端。
做javaweb项目时常用到使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
而在react中使用跨域,可以通过以下方法实现:
https://create-react-app.dev/docs/proxying-api-requests-in-development/
(1) npm install http-proxy-middleware --save
(2) src目录下建立setupProxy.js
(3) 将模板代码复制到文件中,然后根据具体需求作修改(targer是目标地址,'/api'是目标地址后的前缀,如:http://localhost:5000/api/xxx/xxx)
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:5000',
changeOrigin: true,
})
);
};
2.7 iframe
三种:
- window.name + iframe
- location.hash + iframe
- document.domain + iframe