spring web项目跨域问题详解

背景

最近在做javaweb项目的后端服务,涉及到了h5页面跨域访问后端服务的问题,解决之后总结一下分享给大家

什么是跨域

由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。对,万恶的始作俑者就是浏览器的同源策略在限制跨域请求,所以一旦我们做前后端分离的web项目的时候,提供的后端服务有可能被不同的前端地址访问时,就会碰到跨域问题了。
在这里插入图片描述

CORS

cors是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing) 是现代浏览器支持跨域资源请求的一种方式。CORS有两种请求,简单请求和非简单请求。
简单请求需要同时满足下面三个条件:
1.请求方式只能是:GET、POST、HEAD
2.HTTP请求头限制这几种字段:Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID
3.Content-type只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain
否则有一条不满足的话,就是非简单请求了
划重点,也就是说如果你的请求是一个post请求并且参数是application/json类型的话就不是简单请求了哟。
那么简单请求和非简单请求浏览器又分别会怎么处理呢?

简单请求

对于简单请求,浏览器直接请求,会在请求头信息中,增加一个origin字段,来说明本次请求来自哪个源(协议+域名+端口)。服务端根据这个值,来决定是否同意该请求,服务器返回的响应会多几个头信息字段:
1.Access-Control-Allow-Origin: 该字段是必须的,*表示接受任意域名的请求,也可以指定域名。
2.Access-Control-Allow-Credentials:该字段可选,是个布尔值,表示是否可以携带cookie,(注意:如果Access-Control-Allow-Origin字段设置*,此字段设为true无效)
3.Access-Control-Allow-Headers:该字段可选,里面可以获取Cache-Control、Content-Type、Expires等,如果想要拿到其他字段,就可以在这个字段中指定。
4.Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

非简单请求

对于非简单请求,浏览器除了做上面的事情之外,还会在正式发请求之前先发一个OPTIONS预检请求来探探路……浏览器通过OPTIONS请求先询问服务器,当前网页所在域名是否在服务器的许可名单之中,服务器允许之后,浏览器才会发出正式的请求,否则会报错,不会发正式请求了。另外要注意OPTIONS请求是不带cookie和请求的参数的。
所以再次划重点,跨域的OPTIONS请求一定要认真对待哦,不要直接丢弃不管或者返回失败等code。
可以看下面一个模拟的跨域请求,一次请求,实际上浏览器发了两个请求,第一个是OPTIONS请求,并且第一个请求收到200以后才会发第二个请求,而第二个才是我们真正要发的POST请求。
在这里插入图片描述
在这里插入图片描述

后端服务如何支持跨域

前面一大堆介绍之后,大家应该知道我们要做的事情了,简而言之就是两件事:
第一,要能返回Access-Control-Allow-Origin 等字段,告诉浏览器你这个请求域名我服务端是可以支持的。
第二,对于跨域的预检请求,要能返回成功。

几种跨域的解决方案及分析

1.接口层面的解决方案

如果只有某些接口想支持跨域,并且这些接口都是简单请求的(也就是没有预检请求的话),那么可以使用该方法。
直接在请求的response中设置

	response.addHeader("Access-Control-Allow-Origin", requestDomain);
	response.addHeader("Access-Control-Allow-Credentials", "true");
	response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
	response.addHeader("Access-Control-Allow-Headers", "content-type, x-requested-with");
	response.addHeader("Access-Control-Max-Age", "30");
	response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // 支持HTTP1.1
	response.setHeader("Pragma", "no-cache"); // 支持HTTP1.0

但是,问题来了,为什么我要强调简单请求呢,这就要从spring的代码讲起了,并且spring4.1及以前的版本和spring4.2及以后的版本还不一样。下面会分别以spring3.2.3和4.3.11这两个版本来分别讲解它们对预检请求的不同处理。

spring 3.2.3

在这里插入图片描述
这是FrameworkServlet类的doOptions请求的实现,也就是真正处理预检请求的方法,其中会判断dispatchOptionsRequest 这个属性是否为true,为true才会进入processRequest方法中去执行,否则就执行下父类的HttpServlet的doOptions方法就return了。而HttpServlet的doOptions方法基本没干啥,就是设置了Allow这个header而已。真正会发送给springmvc来处理这个请求的代码逻辑都在processRequest方法中执行的!!!
processRequest方法中会去执行DispatcherSevlet的doService方法,而doService方法中又会去调用doDispatch方法,而下图中可以看到doDispatch方法中的1和2,1调用了我们实现的以及系统自带的各种interceptor,2去调用了最后实际的请求方法。
在这里插入图片描述
那么dispatchOptionsRequest这个关键的属性是true还是false呢?当然默认是false了
在这里插入图片描述
所以如果是预检请求,根本走不到我们自己实现的interceptor中以及请求的代码中就已经返回了。在这里插入图片描述
所以很多盆友想用方法1和下面的方法2(拦截器)来解决非简单请求的跨域问题的时候,就会发现根本没有生效啊,网上说的是骗人的吧,为什么到我这里就没用呢!!!
当然是因为你根本没走寻常路啊,你没事写什么非简单请求呢!!你写非简单请求也就算了,用什么spring4.1以下版本呢!!
在这里插入图片描述
那么这种情况要怎么解决呢?其实知道原理以后就很好解决了——就是不用方法1了呀哈哈哈哈哈!!!(当然也不是无解,只是这样也太蠢了,我不想写这样的蠢代码[捂脸])针对方法2怎么解决请看下一节的解说了。

下面我们看下spring4.3.11版本是如何优雅的支持跨域呢

spring 4.3.11

在这里插入图片描述
还是熟悉的FrameworkServlet的doOptions方法,但是味道仿佛有一些变化,增加了是否是跨域options请求的判断!!!如果是跨域options请求不会直接走了,而是也会进入processRequest方法中去执行了!!!简直完美啊!!!而且spring自己提供了预检请求的interceptor和process类,既不会让预检请求走到我们自己接口实现中导致预检请求被当作正式请求处理,也能完美处理跨域预检请求,简直不能更美了!!!

2.springmvc拦截器interceptor

上面说到在spring4.1版本以下的时候简单自己实现一个intercaptor并加到mvc:interceptors标签里是不够的,因为dispatchOptionsRequest属性为false是不会走到interceptor里去的。那么需要怎么实现呢?需要以下几步:
1.设置servlet的dispatchOptionsRequest属性为true
在这里插入图片描述
2.自己实现一个处理跨域请求的interceptor,代码可以参考下面的例子

public class CorsInterceptor extends HandlerInterceptorAdapter {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object Handler) throws Exception {
        if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            HttpServletRequest httprequest = (HttpServletRequest) request;
            HttpServletResponse httpresponse = (HttpServletResponse) response;
            if (isCorsRequest(request)) {
                if (processRequest(request, response) && isPreFlightRequest(request)) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean processRequest(HttpServletRequest request, HttpServletResponse response) {
        String[] allowDomains = {"http://127.0.0.1"}; //自己定义
        String requestDomain = request.getHeader("origin");
        if (isLocalRequest(request))
            return true;
        for (String domain : allowDomains) {
            if (StringUtils.isNotEmpty(requestDomain) && requestDomain.indexOf(domain) >= 0) {
                response.addHeader("Access-Control-Allow-Origin", requestDomain);
                response.addHeader("Access-Control-Allow-Credentials", "true");
                response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
                response.addHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With");
                response.addHeader("Access-Control-Max-Age", "30");
                response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // 支持HTTP1.1
                response.setHeader("Pragma", "no-cache"); // 支持HTTP1.0
                return true;
            }
        }
        return false;
    }

    public  boolean isCorsRequest(HttpServletRequest request) {
        return (request.getHeader("Origin") != null);
    }

    /**
     * Returns {@code true} if the request is a valid CORS pre-flight one.
     */
    public  boolean isPreFlightRequest(HttpServletRequest request) {
        return (isCorsRequest(request) && "OPTIONS".matches(request.getMethod()) &&
                request.getHeader("Access-Control-Request-Method") != null);
    }

    public  boolean isLocalRequest(HttpServletRequest request) {
        String origin = request.getHeader("Origin");
        if (origin == null) {
            return true;
        }
        UriComponents actualUrl = UriComponentsBuilder.fromHttpUrl(request.getRequestURL().toString()).build();
        UriComponents originUrl = UriComponentsBuilder.fromHttpUrl(origin).build();
        return (actualUrl.getHost().equals(originUrl.getHost()));
    }
}

  1. 在mvc:interceptors标签中配置刚刚实现的interceptor
    在这里插入图片描述
    此处可以看到重点在第二步拦截器的实现上。不仅要判断是跨域请求并设置好Access-Control-Allow-Origin等字段以后,还要再判断是否为预检请求,如果是预检请求则需要返回false,结束本次请求,不再继续执行后面的处理了;如果不是预检请求则返回true,继续向后执行。
    为什么要这么处理呢,因为如果是预检请求,但是仍然返回true让这个请求继续被后面执行,如果中间再没有其他的拦截的话,那么最终请求就会落到实际处理请求的controller代码中了,而预检请求虽然请求url是一模一样,但并没有带任何body和cookie过来,所以进入真正处理请求的代码段中一般是不能被正确处理且返回200的。所以正常的预检请求一般都要在正式执行处理请求的代码段之前要提前返回的。

3.servlet过滤器 filter

上面说到在spring4.1以下版本如果只是设置接口层面或者拦截器层面的处理对于非简单跨域请求都会有坑,那么有什么比较万能的解决方式吗?答案当然是有了。那就是用过滤器filter来实现!!!因为filter是被容器(如tomcat)调用的,会先于servlet的service方法之前执行,当filterChain中所有过滤器的dofilter之前的代码都被执行之后才会执行servlet的service方法,然后会反向执行所有过滤器dofilter之后的代码(实质就是递归调用了)。而上面说的servlet的分发拦截等操作都是在servlet的service中执行的,所以使用了过滤器之后妈妈再也不怕我的跨域请求被servlet吃掉了。
在这里插入图片描述
那么如何实现支持跨域的flter呢?操作如下:
1.先实现一个filter接口。
在这里插入图片描述
具体实现跟刚刚的拦截器差不多,只不过处理的代码是要在doFilter方法中调用。
在这里插入图片描述
并且此处也不再需要特殊处理预检请求,只要设置好跨域的头就可以了。因为前面说了预检请求在后面servlet分发的时候就会处理掉了。

4.(福音)Spring 4.2以上版本的方式

前面第1条就提到了spring4.2以上版本对跨域有了完美的支持,那么是否有比第1点接口层面的配置更简洁的处理了,答案当然是有了!!!哈哈哈,可以直接用注解的方式以及mvc的标签方式来配置跨域支持了!!!
1.注解方式:
@CrossOrigin(origins = “*”, maxAge = 3600)
其中
origins:是允许访问的域名列表(origins=“网址”),*表示全部域名。
maxAge:准备响应前的缓存持续的最大时间。
该注解可以在controller层面上配置,也可以在具体方法上使用。
然后spring自带的CorsFilter和CorsUtil等配套设施都会自己处理好了。
2.mvc标签方式
也可以用mvc:cors标签来配置,例如

<mvc:cors>
    <mvc:mapping path="/**" />
</mvc:cors>

或者更复杂配置

<mvc:cors>
   <mvc:mapping path="/**"
             allowed-origins="http://127.0.0.1:8080,http://127.0.0.1:80"
             allowed-methods="GET,POST,PUT,DELETE,OPTIONS"
             allowed-headers="Content-Type,token"
             allow-credentials="true"
             max-age="123" />
</mvc:cors>

总之各种骚操作应有尽有大家可以自行研究,此处不再详细介绍了。

总结

对于spring项目,跨域问题最终极和万能的解决方案还是靠filter来处理,不管是自己实现还是借用spring 的,毕竟filter是执行请求时候最先被执行的。如果放在interceptor中或者controller层面虽然灵活度会更高,但是有可能被前面的filter或者interceptor拦截而导致处理失败。比如一些自己实现的鉴权之类功能的interceptor,由于预检请求是不带cookie和请求body的,所以如果前面的filter和interceptor对这种请求没有很好的支持的话,很有可能就被拦截了。所以大家一定要多检查下,根据自己的需求灵活配置。

参考文章

1:https://www.jianshu.com/p/5cf82f092201
2:https://segmentfault.com/a/1190000015597029
3:https://segmentfault.com/a/1190000010348077
4:https://www.cnblogs.com/pangguoming/p/7402661.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值