前面写的《IT技术人员的自我修养》,没想到几天内收到了不少良好的反馈,在此也感谢大家的关注。往后会不定时分享一些技术、管理领域的工作经验总结与感悟,欢迎大家持续关注、交流。最近被问及一个跨域的问题,包括之前面试时发现很多面试者对跨域及其处理也是一知半解,故本文对该问题进行了梳理总结,以供参考。

1. 什么是跨域

理解什么是跨域,就要先了解一个叫“同源策略”的东西,什么是“同源策略”?这是浏览器为了网站访问安全,对来自不同源的请求做一些必要的访问限制的一种策略。那什么叫“同源”呢?我们知道,一个http请求地址一般包含四部分:协议://域名:端口/路径,所谓同源,就是前面三者,即协议、域名、端口都一样。举例说明,假如我们有一个地址 http://blog.jboost.cn/docker-1.html, 来看以下地址是否与它同源

地址 是否同源 说明
https://blog.jboost.cn/docker-1.html 不同源 协议不同,一个http,一个https
http://www.jboost.cn/docker-1.html 不同源 域名不同
http://blog.jboost.cn:8080/docker-1.html 不同源 端口不同,一个是默认端口80,一个是8080
http://blog.jboost.cn/docker-2.html 同源 虽然路径不同,但协议、域名、端口(默认80)都相同

那么浏览器对不同源的请求做了哪些访问限制呢?共有三种限制

  1. 对Cookie、LocalStorage,以及IndexDB(浏览器提供的类NoSQL的一个本地数据库)的访问
  2. 对DOM的访问
  3. AJAX请求

而跨域就是要打破这种访问限制,对不同源的资源请求也能顺利进行,最常见的就是AJAX请求,比如前后端分离架构中,两者服务域名不同,前端通过AJAX直接访问服务端接口,就会存在跨域问题。

2. 为什么会存在跨域

前面说“同源策略”时已经提到,浏览器是为了网站的访问安全,才设置了跨域这道屏障。那么前面所说的三种限制,分别都是如何来保障网站安全的。

  1. 对本地存储Cookie、LocalStorage、IndexDB的访问限制
    我们系统的登录凭证一般是通过在Cookie中设置 SESSIONID(如针对浏览器表单请求)或直接返回 token(如针对REST请求)的形式返回给客户端的,比如Tomcat是通过在Cookie中设置名为 JSESSIONID 的属性来保存的,而一般REST请求的token前端会存储于 LocalStorage 中,如果不存在访问限制,则你访问的其它网站可能就会获取到这些凭证,然后伪造你的身份来发起非法请求,这就太不安全了。
  2. 对DOM的访问限制
    如果不对DOM进行访问限制,那么其它网站,尤其一些钓鱼网站,就可以通过 <iframe> 的形式拿到你访问网站的DOM,进而获取到你输入的一些敏感信息,比如用户名、密码…
  3. 对AJAX请求的限制
    同源策略规定,AJAX请求只能发给同源的网址,否则就会报错。至于为什么要限制,一方面是避免1中所提到伪造非法请求,另一方面我理解是AJAX过于灵活,如果不做限制,可能网站的接口资源就会被其它网站随意使用,就像你的私有物品被别人招呼都不打任意拿去用一样。

总之,同源策略是浏览器提供的最基本的一种安全保障机制或约定。

3. 怎么实现跨域访问

我们平常遇到的跨域问题基本都出现在AJAX请求的场景,一般而言,可以通过代理、CORS、JSONP等方式来解决跨域问题。

3.1 代理

既然“同源策略”是浏览器端的机制,那我们就可以绕开浏览器,最常见的做法就是使用代理,如 Nginx,比如我们前端项目的域名是 http://blog.jboost.cn,服务端接口域名是 http://api.jboost.cn,我们在 Nginx 中提供如下配置

1
2
3
4
5
6
7
8
9
10
server{
# 端口
listen 80;
# 域名
server_name blog.jboost.cn;
# 所有 http://blog.jboost.cn/api/xxx 请求都会被转发到 http://api.jboost.cn/api/xxx
location ^~ /api {
proxy_pass http://api.jboost.cn;
}
}

则前端通过AJAX请求服务端接口 http://api.jboost.cn/api/xxx 都可以改为通过 http://blog.jboost.cn/api/xxx 来访问,从而避免不同源的跨域问题。

3.2 CORS

CORS是Cross-Origin Resource Sharing的简写,即跨域资源共享,CORS需要服务端与浏览器同时支持,目前所有浏览器(除IE10以下)都支持CORS,因此,实现CORS,主要就是服务端的工作了。例如在Spring Boot中,我们可通过如下配置注册一个CorsFilter的过滤器来实现跨域支持。

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
@Configuration
@ConditionalOnClass({Servlet.class, CorsFilter.class})
public class CORSAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "corsFilterRegistrationBean")
public FilterRegistrationBean corsFilterRegistrationBean() {
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();

CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.applyPermitDefaultValues();
corsConfiguration.setAllowedMethods(Arrays.asList(CorsConfiguration.ALL));
corsConfiguration.addExposedHeader(HttpHeaders.DATE);

corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

CorsFilter corsFilter = new CorsFilter(corsConfigurationSource);
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(corsFilter);
filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
filterRegistrationBean.addUrlPatterns("/*");

return filterRegistrationBean;
}
}

其实质就是在响应消息的Header中添加几个属性,主要有

  • Access-Control-Allow-Origin 必需,表示允许跨域的请求源,可以是具体的域名,也可以是 * ,表示任意域名
  • Access-Control-Allow-Methods 必需,表示允许跨域访问的HTTP方法,如GET、POST、PUT、DELETE等,可以是 * ,表示所有
  • Access-Control-Allow-Headers 如果请求包括 Access-Control-Request-Headers 头信息,则必需,表示服务器支持的所有头信息字段

3.3 JSONP

JSONP是利用浏览器对JS一些标签(如 <script>, <img>等)的 src 属性不具有同源策略限制的特性实现的,如前端添加

1
<script type="text/javascript" src="http://api.jboost.cn/hello?name=jboost&callback=jsonpCallback"/>

并且定义JS方法 jsonpCallback。服务端接口返回内容需要是JS方法jsonpCallback的调用格式,如jsonpCallback({"name":"jboost"}),这样在jsonpCallback方法中就可以获取服务端实际返回的结果数据{"name":"jboost"}了。
JSONP方式的局限性也很明显,一是只支持GET请求——你没见过哪些<script>, <img>标签是POST请求吧,二是需要对服务端返回数据格式做处理。

4. 总结

三种跨域支持的实现,代理方式最简单,对客户端、服务端都不具有侵入性,但如果需要支持的请求源比较多,或者是与第三方对接的话,代理方式就不太适用了。CORS相对来说是一种标准的处理方式,并且通过过滤器的方式对业务代码也没有任何侵入性。而JSONP方式局限性较大,只支持GET,并且需要服务端做返回数据格式的支持。可针对具体情况选择适用的方式。



微信公众号