一、背景
因为是前后端分离开发,所以跨域问题一直都遇到,但以前一直使用的解决方案是通过代码控制设置response.setheader来解决的,这是在百度搜索得到的最多的一个结果,大部分的文章博客都是用这个方案来解决的,诚然它也一直在起作用,直到我最近开发一个新项目再次遇到跨域问题,明明已经设置了response还是会发生,我便开始深入探究,于是就有了一下内容,这是一遍真正解决跨域问题的研究成果,不是百度上人云亦云的复制粘贴来的。
二、跨域问题的产生
跨域问题在前后端分离开发的场景下经常发生,那么在什么情况下会确定发生跨域问题呢?就是前后端不同源的时候,同源需要满足三个条件:
1)协议相同 (http https这种)
2)域名相同
3)端口相同
通常场景下我们的前后端虽然部署在同一个服务器,但一般都是前端放在NGINX 后端放在tomcat 端口是不同的,于是就产生了协议相同、域名相同但是端口不同的跨域问题,此处说个题外话,想要避免跨域问题把前后端都放在同一个tomcat里就行了。
三、CORS的两种请求方式
解决跨域有几种方案,一般最常用的是CORS,因为此方案不需要前端改动,事实上前面提到的response.setheader的方式也是CORS,本文也只针对CORS进行讲解。
CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request),只要同时满足以下两大条件,就属于简单请求:
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、 multipart/form-data、text/plain
不满足以上条件的就属于非简单请求,我称之为复杂请求。
四、两种请求方式的实战解决方案
回顾下背景,以前可以解决跨域的方式为何现在就不起作用了呢?原因就是出在了这不同的请求方式上,以前的项目都是简单请求,而现在的项目是复杂请求,解决措施不一样了,
简单请求
简单请求能直接进入到接口里执行它的内部逻辑,执行完正常返回响应,但如果想要响应的结果被正确接收,就需要在响应头里加上跨域的许可Access-Control-Allow,响应头就是Response Header,关于跨域许可的几个参数有:
等等,其中简单请求必须要有的参数是Access-Control-Allow-Origin (允许跨域的源),其他参数请自行查询作用,这里不做开展,因此解决简单请求的跨域的问题只需要在响应头加上 Access-Control-Allow-Origin 参数,例子:
String origin = request.getHeader("Origin");
if (origin == null) {
origin = request.getHeader("Referer");
}
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "*");
复杂请求
复杂请求不会直接进入接口内部执行接口的逻辑,而是会先发送OPTIONS 预检请求,如果预检请求不能被正确响应,就不会进入到方法内部执行逻辑,所以处理简单请求在代码里设置响应头的方法就不适用了,请求根本达到不了方法内部,于是我们需要在执行方法里的代码之前,就处理好响应头,由此不难联想到使用拦截器就能很好的解决问题
复杂请求的处理原理跟简单请求是一样的,只要设置好响应头就可以了,不同的是在哪个层设置响应头,当然对于简单请求使用复杂请求的处理方式也同样适用,复杂请求处理的两种方式:
1)使用拦截器,设置好响应头,其中必须的跨域参数还是Access-Control-Allow-Origin,如果有更改请求头的话也需要带上Access-Control-Allow-Headers(允许的请求头),事实上会引起复杂请求的大部分场景都是因为更改了请求头,发送json数据时需要设置请求头contentType=application/json。 虽然一般的场景只需要这两个参数就够了但还是建议把所有参数都写全,原因可看下面。拦截器例子:
public class HeadersCORSFilter
implements Filter
{
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse)servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(request, servletResponse);
}
public void init(FilterConfig arg0) throws ServletException {
System.out.println("拦截器初始化");
}
}
2)使用@CrossOrigin,spring能流行的一部分原因我相信有标签的功劳,使用spring标签能帮助我们更优雅简洁的编写代码,所以当我了解到有可以解决跨域问题的@CrossOrigin标签时也是毫不犹豫的用了,按照百度上大多数人的说法在方法上面加上@CrossOrigin标签就可以直接解决问题了,再不济就加上@CrossOrigin(origins=”xx”)指明允许跨域的源就可以了,但很遗憾,无论是哪种方法对我都没有作用,于是深入研究@CrossOrigin,查看其源码发现,除了allow-methods外其他四个参数都是有默认值的(注:此处是基于org.springframework 4.23版本)
于是尝试增加allow-methods属性:
@CrossOrigin(methods = {RequestMethod.POST})
结果就真的起作用了,完美解决了跨域问题。网上还有些说法是在requestmapping上指定方法@RequestMapping(method = RequestMethod.GET),其原理是一样的,因为@CrossOrigin会默认支持@RequestMapping声明的所有源和方法类型。
五、结论
产生跨域问题不要着急,首先分析是否需要规避跨域问题,规避方法就是把前后端放在同一个源里,无法规避的,再分析是简单请求还是复杂请求,对于复杂请求是否需要规避,规避方法为变复杂请求为简单请求,改变请求方式不使用超出范围的请求头等,无法规避就处理复杂请求。