# 跨域
# 什么是跨域
跨域是浏览器的同源策略里所衍生出来的词汇,其意为两个origin源的域名、协议、端口号之中有一个不相同,则会造成跨域。
# 同源策略是什么?
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS(跨站代码注入)、CSRF(跨站请求伪造)等攻击。
# 同源规则
- 协议相同 (
http
、https
等) - 域名相同 (
www
、com
、cn
、zh
、org
等) - 端口号相同(
8080
、80
、3000
等)
# 同源示例
以url http://store.company.com/dir/page.html
作参照。
URL | 结果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 同源 | 只有路径不同 |
http://store.company.com/dir/inner/another.html | 同源 | 只有路径不同 |
https://store.company.com/secure.html | 跨域 | 协议不同 |
http://store.company.com:81/dir/etc.html | 跨域 | 端口不同 ( http:// 默认端口是80) |
http://news.company.com/dir/other.html | 跨域 | 主机不同 |
# 同源策略限制的内容有哪些?
# · 同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
# · 但是有三个标签是允许跨域加载资源:
<img src=XXX>
<link href=XXX>
<script src=xxx>
# 为什么需要对跨域做限制?
跨域问题是因同源策略产生的,而同源策略是浏览器最核心最基本的安全手段。即如果不对跨域做限制,将会受到黑客攻击。拿DOM同源策略和XMLHttpRequest同源策略来举例说明。
# 如果DOM可跨域
- 网页中用iframe嵌套一个其他用户需要提交自己信息的页面(例如关系到个人财产的登录页等)
- 因为没有对跨域做限制,便可以获取iframe嵌套页面的DOM节点,即可以获取节点值(例如账号密码,个人隐私)。
# 如果XMLHttpRequest可跨域
- 黑客可以进行CSRF(跨站请求伪造) 攻击。
- 假如用户登陆了 http://mybank.com 页面,浏览器中的cookie保存了银行页返回来的用户标识。
- 同时用户又浏览了黑客做好的假页面,假页面中自执行了AJAX请求,默认将mybank页对应cookie一同发送过去。
- 银行页验证通过,返回带有用户信息的响应报文。这样,用户的个人数据就泄露了。
- 由于ajax是后台执行,用户是无法感知的。
# 关于跨域的特别说明
- 如果是协议和端口造成的跨域问题“前台”是无能为力的。
- 在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。
- 跨域限制并不会限制请求,但会限制响应的返回。
- 归根结底,跨域是为了阻止用户读取到另一个域名下的内容。
- Ajax会获取响应内容,浏览器认为不安全,所以会受到跨域限制。
- form表单提交时不获取新的内容,因此不受限制。
- 跨域并不能完全阻止 CSRF(跨站请求伪造)
# 常见的跨域有哪些?
# JSONP
# · 原理
<script>
标签没有跨域限制,因此可以在该标签上得到其他来源动态产生的JSON数据。但JSONP请求一定需要对方的服务器做支持。
# · 与AJAX的区别
同样都是客户端给服务器端发送请求,从服务器获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求).
# · 优缺点
- 兼容性好,可用于解决主流浏览器的跨域数据访问问题。
- 仅支持get请求。
- 不安全,可能会遭受XSS攻击。
# · 实现
- 声明一个回调,形参为( {url,params,callback} ),形参类型为Object, 放置 请求url, 请求体,及函数名callback
- 创建一个
<script>
标签,src传入跨域的接口请求地址url。向服务器传递函数名(通过字符串拼接) - 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数,对返回的数据进行操作。
/**
* @param url string
* @param params object
* @param callback function
*/
function jsonp({url, params, callback}) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
window[callback] = function (data) {
resolve(data);
document.body.removeChild(script);
};
let strConcatArr = []; // 拼接字符串数组
params = [...params, callback];
for (let key in params) {
strConcatArr.push(`${key}=${params[key]}`); // http://localhost:3000/find?name=李华&callback=show
}
// 字符串拼接,?之后为查找
url = `${url}?${strConcatArr.join("&")}`;
// 将url放入src中
script.setAttribute("src", url);
document.body.appendChild(script);
});
}
// jsonp放入数据,当调用show时,打印出响应值,并返回响应值。
jsonp({
url:"http://localhost:3000/find",
params:{name:"李华"},
callback: "show"
}).then(res=>{console.log(res); return res;})
# CORS
跨源资源共享
(CORS (opens new window))(cross origin sharing stantard)(或通俗地译为跨域资源共享)是一种基于 HTTP (opens new window) 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin (opens new window)(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。
# · 如何开启CORS
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
# · CORS在业务中的使用
因为现在node环境下有了js语法的后端框架,比如express、koa等。因此如果前端做得full-stack工作,也可能会用到CORS。(若非node,则CORS都是交给后端处理的)。这里以express
为例。
前端发送请求
// index.html let xhr = new XMLHttpRequest() document.cookie = 'name=xiamen' // cookie不能跨域 xhr.withCredentials = true // 前端设置是否带cookie xhr.open('PUT', 'http://localhost:4000/getData', true) xhr.setRequestHeader('name', 'xiamen') xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response) //得到响应头,后台需设置Access-Control-Expose-Headers console.log(xhr.getResponseHeader('name')) } } } xhr.send()
express后端得到请求后,做响应处理,其中包含设置CORS跨域。
//server1.js let express = require('express'); let app = express(); app.use(express.static(__dirname)); app.listen(3000);
//server2.js let express = require('express') let app = express() let whitList = ['http://localhost:3000'] //设置白名单 app.use(function(req, res, next) { let origin = req.headers.origin if (whitList.includes(origin)) { // 设置哪个源可以访问我 res.setHeader('Access-Control-Allow-Origin', origin) // 允许携带哪个头访问我 res.setHeader('Access-Control-Allow-Headers', 'name') // 允许哪个方法访问我 res.setHeader('Access-Control-Allow-Methods', 'PUT') // 允许携带cookie res.setHeader('Access-Control-Allow-Credentials', true) // 预检的存活时间 res.setHeader('Access-Control-Max-Age', 6) // 允许返回的头 res.setHeader('Access-Control-Expose-Headers', 'name') if (req.method === 'OPTIONS') { res.end() // OPTIONS请求不做任何处理 } } next() }) app.put('/getData', function(req, res) { console.log(req.headers) res.setHeader('name', 'jw') //返回一个响应头,后台需设置 res.end('put返回数据') }) app.get('/getData', function(req, res) { console.log(req.headers) res.end('get返回数据') }) app.use(express.static(__dirname)) app.listen(4000)
上述代码由
http://localhost:3000/index.html
向http://localhost:4000/
跨域请求.可知CORS跨域主要由后端添加白名单,做一些逻辑操作实现的。
# postMessage
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
TIP
这个方法类似vue中的emit()传值。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- message:将要发送其他window的数据
- 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
- 是一串和message 同时传递的
Transferable
(opens new window) 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
// a.html下的内容
// 内嵌不同源的b页面
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
<script>
function load() {
// 获取b页面Dom节点
let frame = document.getElementById('frame')
// 给b页面发送消息,若第二个形参 协议域名端口号有一处与b页面url不一样,则发送失败。
frame.contentWindow.postMessage('来自a的msg', 'http://localhost:4000') //发送数据
// 接收b的消息
window.onmessage = function(e) { //接受返回数据
console.log(e.data) // 来自b的msg
}
}
</script>
// b.html下的内容
window.onmessage = function(e) {
console.log(e.data) // 来自a的msg
e.source.postMessage('来自b的msg', e.origin)
}
# webSocket
webSocket本身不存在跨域问题,所以我们可以利用webSocket来进行非同源之间的通信。(client与sever的全双工通信,用法有点类似于postmessage)
# · 实现
利用webSocket的API,可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<!--
高级api 不兼容 但是有一个socket.io这个库,是兼容的(一般用这个)
-->
<script type="text/javascript">
let socket = new WebSocket("ws://localhost:3000");//ws协议是webSocket自己创造的
socket.onopen = function(){
socket.send("我叫俞华");
}
socket.onmessage = function(e){
console.log(e.data);//你好,我叫俞华!
}
</script>
</body>
</html>
以node中间件express,起一个服务端.
/*
要使用ws协议,需要装一个ws的包
*/
let express = require("express");
let app = express();
let WebSocket = require("ws");
let wss = new WebSocket.Server({port:3000});
wss.on("connection",function(ws){//先连接
ws.on("message",function(data){//用message来监听客户端发来的消息
console.log(data);// 我叫俞华
ws.send("你好,"+data+"!");
})
})
# nginx反向代理
其原理是通过代理服务器来发送请求。当本地域名端口为 www.domain.com:81时,通过反向代理,就会被 www.domain2.com:8080代理服务器转发出去。从而解决跨域问题。
// 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;
}
}
随后nginx -s reload,重启nginx
// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
服务端中监听8080端口
// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
# nodejs中间件
和nginx反向代理是原理相似的。都是你把请求发给一个媒介,再由媒介发送给目标。他可以直接代理或者其他的手段得到想到的数据,然后返回。
const Koa = require('koa');
// 代理
const Proxy = require('koa-proxy');
// 对以前的异步函数进行转换
const Convert = require('koa-convert');
const app = new Koa();
const server = require('koa-static');
app.use(server(__dirname+"/www/",{ extensions: ['html']}));
app.use(Convert(Proxy({
// 需要代理的接口地址
host: 'http://127.0.0.1:8888',
// 只代理/api/开头的url
match: /^\/api\//
})));
console.log('服务运行在:http://127.0.0.1:7777');
app.listen(7777);
# window.name+iframe
# · 原理
window.name属性在于加载不同的页面(包括域名不同的情况下),如果name值没有修改,那么它将不会变化。并且这个值可以非常的长(2MB)。
# · 步骤
利用window.name的特性,我们可以做以下几个步骤:
- 在A页面通过
iframe
加载B页面。 - B页面获取完数据后,把数据赋值给window.name。
- 因为Onload会触发2次,在第二次时将window.name赋值给anotherOriginData,完成跨域数据获取。
let mark = false;
let ifr = document.createElement('iframe');
ifr.src = "http://127.0.0.1:8888/demo4";
ifr.style.display = 'none';
var anotherOriginData = null; // 用于存储不同源的数据
document.body.appendChild(ifr);
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
ifr.onload = () => {
// iframe 中数据加载完成,触发onload事件
if (mark) {
anotherOriginData = ifr.contentWindow.name;// 这就是数据
} else {
mark = true;
// 修改src指向本域的一个页面(这个页面什么都没有)
ifr.contentWindow.location = "http://127.0.0.1:7777/demo4/proxy.html";
}
}
# location.hash + iframe
# · 实现背景
- 有三个html页面a,b,c
- ab同源,c不同源
- c嵌套在iframe中
- b为c页面中的嵌套iframe
# · 具体实现步骤
- a.html给c.html传一个hash值
- c.html收到hash值后,再把hash值传递给b.html
- b.html将结果放到a.html的hash值中。
// a.html
<iframe src="http://localhost:4000/c.html"></iframe>
<script>
// a页面开启监听
window.onhashchange = function () { //检测hash的变化
console.log(location.hash); // http://localhost:3000/b.html#{"name": "张三"}
}
</script>
// b.html
<script>
//b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
window.parent.parent.location.hash = location.hash;
</script>
// c.html
console.log(location.hash);
let iframe = document.createElement('iframe');
let data = {name:"张三"};
let strData = JSON.stringify(data);
iframe.src = 'http://localhost:3000/b.html#'+strData;
document.body.appendChild(iframe);
# document.domain + iframe
这个跨域方法要求2个域之间的主域名相同,子域不同,比如a.xxx.com
和b.xxx.com。如果不同的话是不行的。
# · 实现
通过js强制给两个页面设置document.domain为基础主域,就实现了同域。
// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
// 设置主域名 zf1.cn
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.html
<body>
hellob
<script>
// 设置主域名 zf1.cn
document.domain = 'zf1.cn'
var a = 100;
</script>
</body>
# 开发期前端该如何解决跨域?
# cors中间件
如果后端开发采用的是node中的服务器开发框架,则可以使用cors中间件来允许不同源跨域。
const express = require("express");
const cors = require("cors"); // cors中间件
const app = express();
app.use(
// 添加允许被跨域的源
cors({
origin:["http://localhost:3000","http://localhost:8081"]
})
)
# 在配置文件中设置代理中转请求
前端只需要请求中转服务器(以vite为例)
// vite.config.js
export default defineConfig({
plugins: [vue()],
server: {
// /api代理
proxy: {
"/api":{
target:"http://localhost:5000", // 转到目标url
changeOrigin: true, // 允许跨域
rewrite: (path) => path.replace(/^\/api/,"") // 将/api重写为"",只留下url
}
}
}
})
# 总结
- 跨域这一现象是由浏览器同源策略所产生的。当两个源的 协议、域名、端口号,其中之一有所不同,则会引起跨域问题。
- 同源策略只出现在浏览器中
- 跨域限制并非限制请求,而是拦截响应返回的数据报文。
- 一般来说nginx反向代理、CORS解决方案会常用一些。
# 参考文章
← 事件循环 前端如何处理10万条数据 →