WEB前端跨域问题解决方案

什么是跨域

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。

广义的跨域:

  • 1.) 资源跳转: A链接.重定向.表单提交
  • 2.) 资源嵌入: <link>.<script>.<img>.<frame>等dom标签,还有样式中background:url().@font-face()等文件外链
  • 3.) 脚本请求: js发起的ajax请求.dom和js对象的跨域操作等

其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。

什么是同源策略?

同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS.CSFR等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

  • 1.) Cookie、LocalStorage 和 IndexDB 无法读取
  • 2.) DOM 和 Js对象无法获得
  • 3.) AJAX 请求不能发送

常见跨域场景

协议/域名/端口不同

URL 说明 是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js
http://www.domain.com/lab/c.js
同一域名,不同文件或路径 允许
http://www.domain.com:8000/a.js
http://www.domain.com/b.js
同一域名,不同端口 不允许
http://www.domain.com/a.js
https://www.domain.com/b.js
同一域名,不同协议 不允许
http://www.domain.com/a.js
http://192.168.4.12/b.js
域名和域名对应相同ip 不允许
http://www.domain.com/a.js
http://x.domain.com/b.js
http://domain.com/c.js
主域相同,子域不同 不允许
http://www.domain1.com/a.js
http://www.domain2.com/b.js
不同域名 不允许

请求头跨域

前端添加自定义的请求头,默认也是不允许的,会提示跨域,后端或Nginx反代允许对应的头即可。

内部网络跨域

还有一种报错为跨域,但并非跨域的问题

报错:

The request client is not a secure context and the resource is in more-private address space private

原因:

Chrome浏览器94及以后的版本,引入了弃用试验Block insecure private network requests,就是当目标站点的ip地址比请求发起者的ip地址更加私密时,会进行阻止。

在 94 版本后,使用跨域请求需要满足两个条件之一即可

  • https 的请求
  • 非 private 的 http 请求

解决:
以下任意一种方法即可

  1. 在Chrome浏览器地址栏搜索chrome://flags,然后在搜索框搜索Block insecure private network requests,最后将其Default改为Disabled
  2. 使用Chrome 94之前版本。
  3. 使用公网IP,不要使用内网IP。
  4. 页面和接口都使用https

这些地址都会认为是私有地址,当 Chrome 解析到请求是这些私有地址的时候,就会禁止

地址块 姓名
127.0.0.0/8 IPv4 环回
10.0.0.0/8 私人使用
172.16.0.0/12 私人使用
192.168.0.0/16 私人使用
169.254.0.0/16 链接本地
::1/128 IPv6 环回
fc00::/7 独特的地址
fe80::/10 链路本地单播
::ffff:0:0/96 IPv4 映射

跨域解决方案

Nginx反代(推荐)

Nginx中在响应中添加如下Header

1
2
3
4
5
6
7
8
location / {
add_header 'Access-Control-Allow-Origin' $http_origin always;
add_header 'Access-Control-Allow-Methods' 'GET,OPTIONS,POST' always;
add_header 'Access-Control-Allow-Headers' '*';
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 36000;
if ($request_method = OPTIONS ) { return 200; }
}

其中:

Access-Control-Max-Age,用来指定本次预检请求的有效期,单位为秒。

上面配置中,有效期是10小时(36000秒),在此期间,不用发出另一条预检请求。

Access-Control-Allow-Origin 只能设置为三种情况:

  • 星号 *
  • 单域名
  • none

同时还有一个限制就是设置为星号的时候,Access-Control-Allow-Credentials 不能设置为true

Access-Control-Allow-Credentials 则一般是服务器用来设置是否允许前端携带Cookies的标志位,withCredentials 是前端用来表示是否给服务器发请求的时候带上Cookies的标志位。

服务端解决(不推荐)

Java后台

1
2
3
response.setHeader("Access-Control-Allow-Origin", "*");  // 若有端口需写全(协议+域名+端口)
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Max-Age", "36000");

下面这个按需配置

1
response.setHeader("Access-Control-Allow-Credentials", "true");

微服务网关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setMaxAge(36000L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}

Nodejs后台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
var postData = '';

// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});

// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);

// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.domain1.com', // 允许访问的域(协议+域名+端口)
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取cookie
});

res.write(JSON.stringify(postData));
res.end();
});
});

server.listen('8080');
console.log('Server is running at port 8080...');

前端解决

VUE本地代理跨域(推荐)

利用node + webpack + webpack-dev-server代理接口跨域。

在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。

注意这样开发过程中是不跨域了,部署后则无法访问,需要Nginx中配置反向代理。

在项目的根目录新建一个vue.config.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
devServer: {
disableHostCheck: true, // 新增该配置项
proxy: {
'/psvmc': {
target: 'http://www.psvmc.cn', //代理接口
changeOrigin: true,
pathRewrite: {
'^/psvmc': '' //代理的路径
}
}
}

}
};

假如我们请求这样写

1
2
3
4
5
this.axios
.get('/psvmc/userlist.json')
.then(function (data) {
console.info(data);
})

就能在本地请求到http://www.psvmc.cn/userlist.json

注意

打包后直接访问接口是无法访问的,这就要求服务器上做Nginx反代。

Nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 80;
server_name abc.psvmc.cn;

location /psvmc/ {
proxy_pass http://www.psvmc.cn/;
}

location / {
root /data/web_front/myblog/;
index index.html;
}

location ~ .*\.(html)$ {
add_header Cache-Control no-store;
}
}

通过jsonp跨域

通常为了减轻web服务器的负载,我们把js.css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

jsonp缺点:只能实现get一种请求。

前端实现

1.)原生实现:

1
2
3
4
5
6
7
8
9
10
11
var script = document.createElement('script');
script.type = 'text/javascript';

// 传参并指定回调执行函数为onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);

// 回调执行函数
function onBack(res) {
alert(JSON.stringify(res));
}

服务端返回如下(返回时即执行全局函数):

1
onBack({"status": true, "user": "admin"})

2.)jquery ajax:

1
2
3
4
5
6
7
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "onBack", // 自定义回调函数名
data: {}
});

3.)vue.js:

1
2
3
4
5
6
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'onBack'
}).then((res) => {
console.log(res);
})

后端实现

后端node.js代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;

// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');

res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

设置同父级域跨域

document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

1.)父窗口:(www.domain.com/a.html))

1
2
3
4
5
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>

2.)子窗口:(child.domain.com/b.html))

1
2
3
4
5
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>

location.hash + iframe跨域

实现原理: 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页面所有对象。

1.)a.html:(www.domain1.com/a.html))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

2.)b.html:(www.domain2.com/b.html))

1
2
3
4
5
6
7
8
9
<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>

3.)c.html:(www.domain1.com/c.html))

1
2
3
4
5
6
7
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>

window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

1.)a.html:(www.domain1.com/a.html))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
});

2.)proxy.html:(www.domain1.com/proxy….)
中间代理页,与a.html同域,内容为空即可。

3.)b.html:(www.domain2.com/b.html))

1
2
3
<script>
window.name = 'This is domain2 data!';
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
d.) 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为”*”,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。

1.)a.html:(www.domain1.com/a.html))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};

// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>

2.)b.html:(www.domain2.com/b.html))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);

var data = JSON.parse(e.data);
if (data) {
data.number = 16;

// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>

WebSocket协议跨域(不推荐)

不推荐

WebSocket protocol是HTML5一种新的协议。

原来WebSocket根本不附属于同源策略,而且它本身就有意被设计成可以跨域的一个手段。

1.)前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div>user input:<input type="text"></div>
<script src="./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后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var http = require('http');
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.');
});
});