# 常见的跨域场景及相应的解决方法
Author:程村景
Creation Date:2022-5-16
Update Notes:
- 暂无
2022-06-16:培训结束
为什么会跨域?
1. 造成跨域的原因
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源.这里的跨域是广义的.
广义的跨域包括:
* 资源跳转: 链接,重定向,表单提交
* 资源嵌入: link,script,img,iframe等 DOM 标签
* 脚本请求: javascript 发起的 Ajax 请求等
而我们常说的跨域是狭义的,是由浏览器同源策略引起的一类请求场景.
如果两个域名的协议,域名,端口都相同,那我们就说这两个域名是同源的.
2. 同源策略限制些什么?
不能向工作在不同源的服务请求数据,即不能发送 Ajax 请求;
无法获取不同源的 document / cookies 等 BOM 和 DOM,可以说任何有关另一个源的信息都无法得到.
3. 为什么会有同源策略
3.1 为什么要限制不同源发送请求
假设两个页面,a 页面和 b 页面.如果没有任何限制,b 页面可以向 a 页面请求任何信息,那如果 a 页面是个银行之类的页面,那就可以进行转账之类的请求.
那既然如此,为什么不限制写,只限制读?
因为如果连请求都发送不出去,那就不能做跨域资源共享了.
3.2 为什么限制跨域的 DOM 读取?
如果不加以限制,很容易通过 iframe 伪装其网站.进而可以获取用户的登录信息等.
# 跨域的解决方式
# CORS跨域
服务器设置:
ACCESS-CONTROL-ALLOW-ORIGIN: *
只要浏览器检测到响应头带上了 CORS,并且允许的源包括了本网站,那么就不会拦截请求响应。
CORS 分为"==简单请求=="以及"==预检请求==":
- 简单请求 使用下列方法之一:
GET
POST
HEAD 以及满足 Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。 而且 Content-Type 的值仅为下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
- 预检请求 与上述简单请求不一样."需预检的请求"要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器, 以获知服务器是否允许该实际请求。"预检请求"的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。 当满足以下任一条件时,即应首先发送预检请求:
非简单请求的方法 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。 非简单请求的 Content-Type 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。 请求中使用了ReadableStream对象。 另外,还有附带身份验证的请求,也就是携带 cookies. 这个需要前端在请求实例中设置:
// 将 XMLHttpRequest 的 withCredentials 标志设置为 true
const xml = new XMLHttpRequest();
xml.withCredentials = true;
这时,后端需要作出相应的设置.
# JSONP跨域
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个script元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
首先,网页动态插入script元素,由它向跨源网址发出请求。
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面代码通过动态添加script元素,向服务器example.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的。
服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
foo({
"ip": "8.8.8.8"
});
由于script元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"==协议 + 域名 + 端口=="。也可以设为*,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法类似。
window.opener.postMessage('Nice to see you', 'http://aaa.com');
// 父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
message事件的事件对象event,提供以下三个属性。
- event.source:发送消息的窗口
- event.origin: 消息发向的网址
- event.data: 消息内容 下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
==event.origin==属性可以过滤不是发给本窗口的消息。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
if (event.origin !== 'http://aaa.com') return;
if (event.data === 'Hello World') {
event.source.postMessage('Hello', event.origin);
} else {
console.log(event.data);
}
}
# window.name跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。 a.html:(www.domain1.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(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.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.domain2.com/b.html', function(data){
alert(data);
});
proxy.html:(www.domain1.com/proxy.html)
中间代理页,与a.html同域,内容为空即可。
b.html:(www.domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
var data = document.getElementById('myFrame').contentWindow.name;
这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。
# location.hash跨域
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用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.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.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.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.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.domain1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
# document.domain跨域
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie。
举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。
document.domain = 'example.com'; 现在,A网页通过脚本设置一个 Cookie。
document.cookie = "test1=hello"; B网页就可以读到这个 Cookie。
var allCookie = document.cookie; 注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/ 这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
# nginx 反向代理接口跨域
跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。
实现思路:通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。 nginx具体配置:
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;
}
}
# nodejs中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
使用==node + express + http-proxy-middleware==搭建一个proxy服务器。 前端代码:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
中间件服务器代码
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
# WebSocket跨域
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
原理:这种方式本质没有使用了 HTTP 的响应头, 因此也没有跨域的限制。
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。 在网络浏览器和服务器之间建立“套接字”连接。简单地说:客户端和服务器之间存在持久的连接,而且双方都可以随时开始发送数据。 前端代码:
<script>
let socket = new WebSocket("ws://localhost:8080");
socket.onopen = function() {
socket.send("holle");
};
socket.onmessage = function(e) {
console.log(e.data);
};
</script>
后端代码:
const WebSocket = require("ws");
const server = new WebSocket.Server({ port: 8080 });
server.on("connection", function(socket) {
socket.on("message", function(data) {
socket.send(data);
});
});
# 浏览器开启跨域
其实跨域问题是浏览器策略,源头是他,关闭这个功能
Windows 找到你安装的目录
.\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx
Mac ~/Downloads/chrome-data这个目录可以自定义.
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --disable-web-security --user-data-dir=~/Downloads/chrome-data