前面写的《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)都相同 |
那么浏览器对不同源的请求做了哪些访问限制呢?共有三种限制
-
对Cookie、LocalStorage,以及IndexDB(浏览器提供的类NoSQL的一个本地数据库)的访问
-
对DOM的访问
-
AJAX请求
而跨域就是要打破这种访问限制,对不同源的资源请求也能顺利进行,最常见的就是AJAX请求,比如前后端分离架构中,两者服务域名不同,前端通过AJAX直接访问服务端接口,就会存在跨域问题。
2. 为什么会存在跨域
前面说“同源策略”时已经提到,浏览器是为了网站的访问安全,才设置了跨域这道屏障。那么前面所说的三种限制,分别都是如何来保障网站安全的。
-
对本地存储Cookie、LocalStorage、IndexDB的访问限制
我们系统的登录凭证一般是通过在Cookie中设置 SESSIONID(如针对浏览器表单请求)或直接返回 token(如针对REST请求)的形式返回给客户端的,比如Tomcat是通过在Cookie中设置名为 JSESSIONID 的属性来保存的,而一般REST请求的token前端会存储于 LocalStorage 中,如果不存在访问限制,则你访问的其它网站可能就会获取到这些凭证,然后伪造你的身份来发起非法请求,这就太不安全了。 -
对DOM的访问限制
如果不对DOM进行访问限制,那么其它网站,尤其一些钓鱼网站,就可以通过<iframe>
的形式拿到你访问网站的DOM,进而获取到你输入的一些敏感信息,比如用户名、密码… -
对AJAX请求的限制
同源策略规定,AJAX请求只能发给同源的网址,否则就会报错。至于为什么要限制,一方面是避免1中所提到伪造非法请求,另一方面我理解是AJAX过于灵活,如果不做限制,可能网站的接口资源就会被其它网站随意使用,就像你的私有物品被别人招呼都不打任意拿去用一样。
总之,同源策略是浏览器提供的最基本的一种安全保障机制或约定。
3. 怎么实现跨域访问
我们平常遇到的跨域问题基本都出现在AJAX请求的场景,一般而言,可以通过代理、CORS、JSONP等方式来解决跨域问题。
3.1 代理
既然“同源策略”是浏览器端的机制,那我们就可以绕开浏览器,最常见的做法就是使用代理,如 Nginx,比如我们前端项目的域名是 http://blog.jboost.cn,服务端接口域名是 http://api.jboost.cn,我们在 Nginx 中提供如下配置
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 @Configuration 2 @ConditionalOnClass({Servlet.class, CorsFilter.class}) 3 public class CORSAutoConfiguration { 4 5 @Bean 6 @ConditionalOnMissingBean(name = "corsFilterRegistrationBean") 7 public FilterRegistrationBean corsFilterRegistrationBean() { 8 UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); 9 10 CorsConfiguration corsConfiguration = new CorsConfiguration(); 11 corsConfiguration.applyPermitDefaultValues(); 12 corsConfiguration.setAllowedMethods(Arrays.asList(CorsConfiguration.ALL)); 13 corsConfiguration.addExposedHeader(HttpHeaders.DATE); 14 15 corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); 16 17 CorsFilter corsFilter = new CorsFilter(corsConfigurationSource); 18 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); 19 filterRegistrationBean.setFilter(corsFilter); 20 filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); 21 filterRegistrationBean.addUrlPatterns("/*"); 22 23 return filterRegistrationBean; 24 } 25 }
其实质就是在响应消息的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是利用浏览器对HTML一些标签(如 <script>
, <img>
等)的 src 属性不具有同源策略限制的特性实现的,如前端添加
<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,并且需要服务端做返回数据格式的支持。可针对具体情况选择适用的方式。