文章目录
一.同源策略
1.同源策略:
-
同源策略是一种约定,它是浏览器最核心也最基本的安全功能。
-
请求资源时必须保证同源,即"协议+域名+端口"三者必须相同,否则浏览器会因为安全性问题拦截请求到的数据。
-
如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。
2.同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- Ajax请求
3.有三个标签是允许跨域加载资源:
<img src=XXX>
<link href=XXX>
<script src=XXX>
4.⭐️ 请求跨域了,那么请求到底发出去没有?
即使跨域了,请求也会发出去,服务端会收到请求并返回结果,只是最后结果被浏览器拦截了
二.跨域的解决方案:
- 实现跨域的核心:绕过同源政策的限制
1.针对Ajax跨域请求数据的解决方案:
(1)jsonp方案:
原理:
- 利用
<script>
标签可以跨域访问的特性,可以从服务器返回一段执行某函数的代码,并向里面传递结果数据。 - jsonp请求需要对方的服务器做支持才可以。
jsonp和Ajax对比:
- jsonp和Ajax都是发送请求向服务器请求数据的方法。
- 但Ajax受同源策略的限制,jsonp属于跨域请求
jsonp的优缺点:
-
优点是简单,兼容性好;可用于解决主流浏览器的跨域请求问题
-
缺点:
-
只支持get方法,因为script标签只能发送get请求
-
不安全,可能遭受XSS攻击
-
需要后端配合返回指定格式的数据
-
jsonp的实现流程:
- 定义一个接受数据的回调函数。
- 再封装一个函数,需要传递的参数包括回调函数的名称以及需要访问的url。
- 在函数内部将url和回调函数的名字拼接起来,然后动态创建一个script标签,将src属性设置为拼接的字符串
- 向dom树中添加这个script标签
- 服务器端需要做的事:在拦截到用户请求时,获取请求参数中的回调函数名字,将欲返回的数据和该函数的名字拼接成函数调用的形式,然后返回给客户端。
jsonp方案的基本实现:
客户端:
<body>
<button id="button">点击发送非同源的请求</button>
<!-- 1.在全局作用域下定义函数fn,当加载完后面的script标签后,相当于调用了这个fn函数,所以fn函数里的data就变成了我们想要获取的数据。 -->
<script>
function fn(data) {
console.log(data);
}
</script>
<script>
button.onclick = function () {
let script = document.createElement("script");
script.src = "http://localhost:3001/jsonpBasic";
document.body.appendChild(script);
// 2.存在一个问题:每次点击发送都会为dom添加一个script标签。
// 解决方案:在每次加载完script标签后,将其从html中删除
script.onload = function () {
document.body.removeChild(script);
};
};
</script>
</body>
服务器端:
//1.由于script标签的原理是一加载就会执行里面的代码,故服务器端响应的数据只能是一个执行函数的表达式,这样客户端才能拿到想要的数据。
//2.注意:通过script标签发送的请求都是get请求。
app.get("/jsonpBasic", (req, res) => {
res.send('fn({name:"zhangsan",age:20})');
});
jsonp函数的封装:
一些优化点:
1.每次点击发送都会为dom添加一个script标签
2.客户端和服务端需要商讨函数的名称的问题
3.封装jsonp函数
4.如何省去函数的定义,并且调用Jsonp函数的时候省去url后面的字符串
5.请求参数的问题
6.服务器代码的优化:利用res.jsonp({})可以直接代替所有步骤。。。
客户端:
<button id="button">点击发送非同源的请求</button>
<!-- 1.在全局作用域下定义函数fn,当加载完后面的script标签后,相当于调用了这个fn函数,所以fn函数里的data就变成了我们想要获取的数据。 -->
<script>
// function fn3(data) {
// console.log(data);
// }
button.onclick = function () {
jsonp({
url: "http://localhost:3001/jsonp",
data: {
name: "zhangsan",
age: 20,
gender: "male",
},
success: function (data) {
console.log(data);
},
});
};
// 4.封装jsonp函数,跟封装ajax类似
function jsonp(options) {
let script = document.createElement("script");
let params = "";
for (key in options.data) {
params += `&${key}=${options.data[key]}`;
}
// 5.将传递过来的函数挂载在window对象上,使其成为全局函数,便于加载script标签的时候直接执行这个函数。
// 这里存在一个问题:后发送的请求中挂载的fn2函数会覆盖前面的fn2函数。解决方法是将这个函数的名字设置一个随机数。
//0.1524 注意:函数名不能为纯数字
let functionName =
"myJsonp" + Math.random().toString().replace(".", "");
// window.fn2 = options.success;
window[functionName] = options.success;
// 3.在请求路径中添加所使用的函数的名称,解决需要前后端需要商量函数名的问题
// script.src = options.url + "?callback=fn3";
script.src = `${options.url}?callback=${functionName}${params}`;
document.body.appendChild(script);
// 2.存在一个问题:每次点击发送都会为dom添加一个script标签。
// 解决方案:在每次加载完script标签后,将其从html中删除
script.onload = function () {
document.body.removeChild(script);
};
}
</script>
服务器端:
app.get("/jsonp", (req, res) => {
//1.由于script标签的原理是一加载就会执行里面的代码,故服务器端响应的数据只能是一个函数,这样客户端才能拿到想要的数据。
//2.注意:通过script标签发送的请求都是get请求。
// let functionName = req.query.callback;
// // let data = 'fn({username:"zhangsan",age:20})';
// let data = `${functionName}({username:"zhangsan",age:20})`;
// res.send(data);
// 3.使用res.jsonp({})可以代替所有的步骤
res.jsonp({ username: "zhangsan", age: 20 });
});
(2)CORS方案:
CORS:cross-origin resource sharing跨域资源共享
原理:
- 浏览器允许 由服务器端决定哪些客户端能跨域访问其资源。
- 服务器端会在响应报文中设置Access-Control-Access-Origin字段,表示该资源能被哪些客户端跨域访问。
- 后端是实现CORS通信的关键
origin: http://localhost:3000
Access-Control-Allow-Origin: "*" 表示该资源允许所有的客户端访问
服务器端需要进行的设置:
app.get("/cors", (req, res) => {
// 1.设置该资源能被哪些客户端访问
res.header("Access-Control-Allow-Origin", "*");
// 2.设置访问该资源时所允许的客户端发送请求的方式
res.header("Access-Control-Allow-Methods", "get,post");
res.send({ name: "zhangsan" });
});
优化:利用expree框架的中间件拦截所有的请求,使得客户端发送的所有请求都能实现跨域访问。
app.use((req, res, next) => {
// 1.设置该资源能被哪些客户端访问
res.header("Access-Control-Allow-Origin", "*");
// 2.设置访问该资源时所允许的客户端发送请求的方式
res.header("Access-Control-Allow-Methods", "get,post");
next();
});
(3)nginx反向代理
-
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
-
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题
// proxy服务器
server {
listen 80;
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;
}
}
(4)Node中间件代理(两次跨域)
原理:
- 服务器端不存在同源政策的限制
- 可以由客户端访问本地代理服务器端,由本地代理服务器端去访问2号服务器端的数据
- 本地代理服务器获取数据后再将数据返回给客户端。
如何在node服务器中向另一个node服务器发送请求:
- 需要用到第三方模块,比如’superagent’,在同源的服务器上利用superagent向另一个服务器发送请求。
(5)websocket方案:(直接不使用Ajax,替换为websoket协议 )
原理:
- Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。可以直接跨域访问数据。
具体实现:
-
客户端:
// socket.html <script> let socket = new WebSocket('ws://localhost:3000'); socket.onopen = function () { socket.send('服务器你好啊');//向服务器发送数据 } socket.onmessage = function (e) { console.log(e.data);//接收服务器返回的数据 } </script>
-
服务器端:
// server.js // 原生WebSocket API使用起来不太方便,我们可以使用Socket.io,它很好地封装了webSocket接口 let express = require('express'); let app = express(); let WebSocket = require('ws');//记得安装ws let wss = new WebSocket.Server({port:3000}); wss.on('connection',function(ws) { ws.on('message', function (data) { console.log(data); ws.send('嘿嘿,客户端你好') }); })
2.跨域获取另一个页面(比如iframe)的数据:
(6)postMessage方案
原理:
- postMessage是HTML5提供的新的API,可以安全地实现跨源通信。
- 具体实现是通过window.postMessage发送消息,通过onMessage来监听消息
实际应用场景包括:
- 页面与嵌套的iframe之间的通信
- 多个窗口之间的通信
具体实现:(注意:iframe相当于只提供了一个窗口,通信的实现靠的是postMessage这个API)
- tab1: 引入iframe的页面
<body>
<input type="text" id="input" />
<button onclick="sendMessage()">点击向iframe发送数据</button><br />
<iframe
src="http://127.0.0.1:5500/3.test2.html"
frameborder="0"
allow=""
id="iframe"
></iframe>
</body>
<script>
function sendMessage() {
// 1.获取iframe窗口的引用
// 方法一:直接获取iframe的contentWindow属性
// contentWindow 属性返回当前HTMLIFrameElement的Window对象. 你可以使用这个Window 对象去访问这个iframe的文档和它内部的DOM
// var win = iframe.contentWindow;
// 方法二:通过window.frames获取
// 返回的是frame对象的集合,一个类数组对象,列出了当前窗口的所有直接子窗口
var win = frames[0];
// 2.向指定的窗口发送数据
//otherWindow.postMessage(message, targetOrigin);
//2.1 otherwindow表示指定的窗口,可以有三种情况:
//可以是iframe的contentWindow属性
//也可以是window.open返回的窗口对象
//还可以是window.frames中的子窗口
//2.2 第一个参数message表示要发送的消息
//2.3 第二个参数表示目标窗口的地址。注意:如果是/,则表示只发送消息给同源的页面;如果是*就表示将数据发送给全部页面。
win.postMessage(input.value, "http://127.0.0.1:5500");
}
</script>
- tab2: iframe原页面
<body>
<div
id="iframe"
style="width: 500px; height: 500px; background-color: skyblue"
>
这是内嵌的iframe
</div>
</body>
<script>
// 监听通过postMessage向自己发来的数据
window.addEventListener("message", function (e) {
console.log(e);
// e.target表示发送方的原地址
// e.data表示发送过来的数据
// e.source表示对发送消息的窗口对象(即window对象)的引用,可以利用继续使用这个属性向原地址返回数据
// e.source.postMessage()
iframe.innerHTML = e.data;
});
</script>
(7)window.name + iframe
前置条件:
- iframe中可以加载跨域的资源,但是获取iframe中的数据是属于跨域的。
- ⭐️ 核心:在同一个窗口中,即使页面被换为了其他页面,该窗口的window.name属性依旧不变。
原理:
- 首先让iframe加载跨域的页面,此时跨域的页面需要做的就是将数据存放在window.name属性中
- 然后让iframe中的src指向一个同源的空页面,此时由于是同源的情况,所以可以通过iframe.contentWindow访问到里面的name属性,这样就拿到了跨域页面传递过来的数据
具体代码:
- 原页面:http://127.0.0.1:5500/1.test.html
<body>
<iframe frameborder="0" id="iframe1"></iframe>
<script>
console.log(iframe1.contentWindow.name); //输出为空----由于跨域,不能直接获取跨域页面的属性
let tag = 0;
iframe1.src = "http://localhost:3000/index2.html"; //首先加载跨域的页面,使其将数据存储在window.name中
iframe1.onload = function () {
if (tag == 0) {
iframe1.src = "http://127.0.0.1:5500/proxy.html"; //将src的路径切换为同源的路径,此时由于是同源的缘故,可以获取其中窗口的window.name属性
tag = 1;
} else {
console.log(JSON.parse(iframe1.contentWindow.name));//{name: "liu", age: 21, gender: 1}
}
};
</script>
</body>
- 中间代理页面:http://127.0.0.1:5500/proxy.html
<body></body> 是个空页面
- 跨域访问的页面:http://localhost:3000/index2.html
<body>
这里是跨域请求的iframe页面
<script>
let obj = {
name: "liu",
age: 21,
gender: 1,
};
window.name = JSON.stringify(obj);
</script>
</body>
(8)location.hash + iframe
前置条件:
- 当一个窗口的hash(url后面的#部分)改变时,会触发hashchange事件
原理:
- 首先通过iframe加载跨域的页面
- 跨域的页面创建一个iframe,src设置为与原来页面同源的一个页面。关键是需要url后添加一个hash,值为传递的数据
- 同源的代理页在被加载时,需要将其身上的location.hash设置在父级window的父级window的location.hash上。
- 当原页面监听到hashchange事件时,从中取出location.hash就可以得到最终的数据
具体实现:
-
原页面:http://127.0.0.1:5500/1.test.html
<iframe frameborder="0" id="iframe1"></iframe> <script> iframe1.src = "http://localhost:3000/index2.html"; // 原页面需要监听hash值得改变,即监听此时代理页是否将location.hash设置好了 window.addEventListener("hashchange", function (e) { console.log(location.hash.substring(1)); }); </script>
-
跨域请求的页面:http://localhost:3000/index2.html
<body> 这里是跨域请求的iframe页面 <script> let obj = { name: "liu", age: 21, gender: 1, }; var iframe = document.createElement("iframe"); iframe.src = "http://127.0.0.1:5500/proxy.html" + "#" + JSON.stringify(obj); //在服务器端创建一个iframe,src指回原来的域名,hash值设置为响应的数据 document.body.appendChild(iframe); </script> </body>
-
同源的代理页面:http://127.0.0.1:5500/proxy.html
<body> <script> // 代理页即为服务器端内嵌的iframe // 其中可以访问到传递过来的hash值,可以将其设置在上级window的上级window的location.hash属性中 window.parent.parent.location.hash = location.hash.substring(1); </script> </body>
(9)document.domain + iframe
前置条件:
- 若两个页面的域名必须属于同一个基础域名,而且所用的协议,端口都要一致,则可以实现跨域访问。
- 具体实现就是通过js强制性的将两个页面的document.domain属性设置为相同的主域名。
具体实现:
-
原页面:http://a.zf1.cn:3000/a.html
<body> <iframe src="" id="iframe"></iframe> <script> document.domain="zf1.cn"; iframe.src="http://b.zf1.cn:3000/b.html"; iframe.onload=function(){ console.log(iframe.contentWindow.age)//21 } </script> </body>
-
跨域的页面: http://b.zf1.cn:3000/a.html
<body> <script> document.domain="zf1.cn"; var age=21; </script> </body>
3.总结
- CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
- jsonp只支持GET请求,JSONP的优势在于兼容性很好。
- 日常工作中,用得比较多的跨域方案是CORS和nginx反向代理
- 不管是Node中间件代理还是nginx反向代理,利用的都是服务器端不存在同源政策的限制