跨域问题及解决方案

前言

要了解跨域问题,我们先来了解下浏览器的同源策略。

浏览器的同源策略限制了从同一个源加载的文档或脚本与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

同源的定义:

如果两个URL的协议、端口、IP(域名)都相同,则这两个URL具有相同的源。

根据这个定义,我们给出了下面的表格,可以简单看一下。

URL AURL B是否同源原因
http://abc.xyz.kk:8080/index.htmlhttp://abc.xyz.kk:8080/demo/demo.html协议、端口、域名均相同
http://abc.xyz.kk:8080/apihttp://abc.xyz.kk:8888/index.html端口不相同
http://abc.xyz.kk:8080/apihttp://xyz.abc.kk:8080/index.html域名(IP)不相同
http://abc.xyz.kk:8080/index.htmlhttps://abc.xyz.kk:8888/index.html协议不相同
http://abc.xyz.kk:8080/hellohttps://mmm.sss.ll:8888/hello协议、端口、域名均不相同

跨域问题

看到前言所说,同源策略提高了数据安全性,为什么我们还要研究跨域问题呢?

如果单纯的Web网站,页面资源(html、js或jsp等)在服务端,我们是不用考虑跨域问题的,因为它们就在一个域下。

但是现在很多项目是前后端分离的,无论原生APP还是WebApp,由于IP、端口或者协议等的不同,它们的请求在访问后端系统时,如果不做些处理,就会受到浏览器同源策略的约束,进而出现403错误。

跨域方法

我们目前有以下几种方法解决跨域问题,我们分别来看下吧。

Cross-Origin Resource Sharing (CORS)

简介

CORS是一个跨域资源共享方案,为了解决跨域问题,通过增加一系列HTTP请求头和响应头,规范安全地进行跨站数据传输。

请求头主要包括以下参数:

参数名说明
Origin用于在跨域请求或预先请求中,标明发起跨域请求的源域名
Access-Control-Request-Method用于表明跨域请求使用的实际HTTP方法
Access-Control-Request-Headers用于在预先请求时,告知服务器要发起的跨域请求中会携带的请求头信息
withCredentials跨域请求是否携带凭据信息,如果设置为true,响应头的Access-Control-Allow-Origin必须指定具体域名,且Access-Control-Allow-Credentials参数为true

响应头主要包括以下参数:

参数名说明
Access-Control-Allow-Origin该参数中携带了服务器端验证后的允许的跨域请求域名,可以是一个具体的域名或是一个*(表示任意域名)
Access-Control-Expose-Headers该参数用于允许返回给跨域请求的响应头列表,在列表中的响应头的内容,才可以被浏览器访问
Access-Control-Max-Age该参数用于告知浏览器可以将预先检查请求返回结果缓存的时间,在缓存有效期内,浏览器会使用缓存的预先检查结果判断是否发送跨域请求
Access-Control-Allow-Methods该参数用于告知浏览器可以在实际发送跨域请求时,可以支持的请求方法,可以是一个具体的方法列表或是一个*(表示任意方法)
Access-Control-Allow-Credentials是否允许携带凭据信息。默认凭据信息 不包括在 CORS 请求之中

当我们给客户端添加符合的上述请求头参数,给服务端添加符合的响应头参数后,客户端对服务端的请求便可以实现跨越访问。

对CORS有更多兴趣的同学可以参考MDN的这篇文章。

例子

我们使用前端JS+后台SpringBoot的例子来看一下。

前端部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
$("#test").click(function() {
$.ajax({
type: "POST",
url: "http://abc.xyz.kk/hello",
data:{
name:"hello"
},
success: function(result) {
alert(result);
}
});
});

后端部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@Slf4j
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允许所有类型请求头
corsConfiguration.addAllowedHeader("*");
//允许HEAD、POST和OPTIONS方法
corsConfiguration.setAllowedMethods(Arrays.asList("HEAD","POST", "OPTIONS"));
//允许携带Cookie
corsConfiguration.setAllowCredentials(true);
//允许所有的源
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
//时间设置为3600s
corsConfiguration.setMaxAge(3600L);

//跨域设置
//所有请求都允许跨域
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}

一般出现跨域问题,如果使用CORS,只需要后台配置CORS过滤器(如上)即可实现跨域访问。

JSONP

简介

JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。

JSONP的原理就是借助HTML中的<script>标签可以跨域引入资源。所以动态创建一个<srcipt>标签,src为目的接口 + get数据包 + 处理数据的函数名。后台收到GET请求后解析并返回函数名(数据)给前端,前端<script>标签动态执行处理函数。

<script>标签的src属性是没有跨域的限制的。这样说来,这种跨域方式其实与Ajax XmlHttpRequest协议无关了。

例子

我们看一下JSONP方式实现跨域的前后端大致代码:

前端部分代码:

1
2
3
4
5
6
7
<script>
//jsonp回调方法,一定要写在jsonp请求之前
function jsonptest(result){
alert(result);
}
</script>
<script src ="/hello/test?callback=jsonptest" type="text/javascript" ></script>

后端部分代码:

1
2
3
4
5
6
7
8
9
10
@Controller
@RequestMapping("/hello")
public class HelloController{
@RequestMapping(value="test",method=RequestMethod.GET)
@ResponseBody
public String jsonpTest(String callback){
//do something
return callback +"('Hello World!');";
}
}

可以看到如果客户期望返回Hello World! 实际收到的请求为 jsonptest('Hello World!'),然后调用jsonptest函数获得实际想要的结果。

因为JSONP使用js的<script>标签进行传参,故该种方式只支持GET请求,这也是JSONP的一个缺点。

Nginx反向代理

简介

出现跨域限制的根本原因是浏览器同源问题的限制。

我们如果把前端项目和前端要请求的后台API接口地址放在同源下不就可以实现跨域请求了么?

这样我们前后端都不需要做任何跨域配置处理。

例子

比如我们有一个H5项目,部署在 http://abc.xyz.kk:8088 Nginx服务器上,后台地址为 http://abc.123.ss:8888 ,后台项目API接口地址为 http://abc.123.ss:8888/api/

则Nginx的配置文件 nginx.conf 的 server部分配置如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
proxy_pass http://localhost:8000/; # 前端本机地址,实现自动更新
autoindex on;
autoindex_exact_size on;
autoindex_localtime on;
}

location /api/ {
            proxy_pass http://abc.123.ss:8888; # 后台API接口地址
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

当我们访问了 http://abc.xyz.kk:8088 地址(代理前端地址),访问后台API时,通过反向代理,相当于访问 http://abc.xyz.kk:8088/api/ 这个地址,就不会出现跨域问题了。

其他跨域解决方案

简介

如果我们要通过A网页访问另一个域的B网页时,根据同源策略,也会出现跨域问题。

这种情况我们可以通过window.postMessagewindow.name共享、window.location.hash共享等方法来解决,关于这块,我们简单的用window.postMessage来看下,其他的大家可以查询相关资料进行了解。

使用WebSocket也可以实现资源跨域访问,WebSocket是长连接,资源消耗较大,除在一些即时通讯等特殊场景,专门用来解决跨域问题还是少之又少的,这一块我们也不在详述。

例子

我们用window.postMessage来进行举例。

下面是两个HTML,http://aaa.aaa.aa/A.htmlhttp://bbb.bbb.bb/B.html 用 postMessage进行交互的例子。

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
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>A.html</title>
</head>
<body>
<div>
<input id="test" type="text" value="B.html" />
<button id="send" >A发送消息给B</button>
</div>
<iframe id="receiver" src="http://bbb.bbb.bb/B.html" width="500" height="60">
<p>你的浏览器不支持IFrame。</p>
</iframe>
<script>
window.onload = function() {
var receiver = document.getElementById('receiver').contentWindow;
var btn = document.getElementById('send');
btn.addEventListener('click', function (e) {
e.preventDefault();
var val = document.getElementById('test').value;
receiver.postMessage("Hello "+val+"!", "http://aaa.aaa.aa/A.html");
});
}
</script>
</body>
</html>

B.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
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>B.html</title>
</head>
<body>
<div id="message">
Hello World!
</div>
<script>
window.onload = function() {
var messageEle = document.getElementById('message');
window.addEventListener('message', function (e) {
alert(e.origin);
if (e.origin !== "http://aaa.aaa.aa/A.html") {
return;
}
messageEle.innerHTML = "从"+ e.origin +"收到消息: " + e.data;
});
}
</script>
</body>
</html>

总结

以上跨域解决方案,最常用的还是CORS和反向代理,其次是JSONP,其他很少会被使用。

在允许Ajax XmlHttpRequest的浏览器(高版本浏览器)并与后端交互的场景,CORS和反向代理应用最广。

如果浏览器不支持XmlHttpRequest(IE6、IE7….),可以考虑使用JSONP。

如果涉及到不同源网页交互,支持H5的浏览器可以采用window.postMessage,不支持的可以使用window.name共享、window.location.hash共享等。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道