1、问题背景:
公司在做自己的OA系统,采用的前后端分离的开放模式,在与前端对接"获取当前用户信息"这个接口时,浏览器的控制台就报了以下错误:
Access to XMLHttpRequest at 'http://192.168.0.101:10000/api/login/login?username=1&password=1' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
我们采用token来维护用户登录状态的方案,用户登录成功后,以后每次需要登录的接口都需要在header中带着这个token,后端用springMVC的拦截器做登录拦截,一切看着都是这么的自然,然而......
第一个对接的是"登录接口",很顺利的通过了。当前端调用第二个接口——"获取当前用户信息"接口时,突然前端小姐姐叫了一声:"哎?这是什么错误?是不是跨域了?"。我当时心想:"不可能呀,我在搭框架的时候已经在后端处理了跨域问题"。下面是我的解决方式:
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //表示所有的资源都允许跨域访问
.allowedOrigins("*") //表示所有的源都可以访问
表示允许那些http请求方式
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true) //表示请求中可以携带cookie
.maxAge(3600) //表示预检请求的有效期,有效期内不会在发送第二次预检请求。
.allowedHeaders("*"); //表示允许所有请求头
}
标准的CORS解决方式,也确实能解决跨域问题,但是并不适合我这里的场景,为什么?因为我使用了拦截器。为什么使用了拦截器就不行了?下面听我分析一下:
2、问题原因:
为什么第一个登录接口没事,第二个就有跨域问题?因为第一个登陆接口我没做登录拦截,会执行到上面的方法(此时该登录请求是一个简单请求)。第二个接口做了登录拦截,##但我在请求头中加入了token值,还是应该可以通过拦截器到达目标方法,也会执行上面的跨域解决方法,为什么还是被拦截出现了跨域问题呢?##OK,问题就出现在这里,就是因为我在请求头中加入了token,导致了该请求从一个简单请求变成了非简单请求,至于什么是非简单请求暂时先不说?但他有一个区别于简单请求的最大区别:会在正式请求发送之前,先发送一个预检请求,就是个普通请求,用于检查目标服务器是否允许当前源跨域访问。该预检请求可是什么参数都不带的,自然就被我的登录拦截器给拦下了,然后回去告诉客户端浏览器说目标服务器不允许你跨域访问,浏览器说了声,哦~,然后就向前端小姐姐抛了一个错误,可是把小姐姐难住了~~
3、解决方案:
既然该预检请求会被拦截器拦截,导致无法执行我解决跨域的方法,那我只需要在拦截器拦截之前,给请求的响应头加入允许跨域访问的响应头即可。springMVC的本质是servlet,当时学javaWeb时知道filter的执行时机是在servlet之前,那我使用filter来增加响应头就行了呗。
自己写一个filter也行,但其实springMVC早就提供了这么一个解决跨域的filter——CorsFilter。若是springBoot项目的话,直接在配置类中加入该bean即可。
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");//允许所有的源访问
config.setAllowCredentials(true);//允许请求中带着cookie
config.addAllowedMethod("*");//允许哪些http请求方式
config.addAllowedHeader("*");//允许请求头中额外带着哪些请求头
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);//表示所有的资源都允许跨域访问
return new CorsFilter(configSource);
}
其他的解决方式也有很多,这里我再提供几种后端解决跨域的方式:
1)自定义filter
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE,PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
filterChain.doFilter(servletRequest, servletResponse);
}
2)重写springMVC的addCorsMappings方法
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //表示所有的资源都允许跨域访问
.allowedOrigins("*") //表示所有的源都可以访问
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") //表示允许那些http请求方式
.allowCredentials(true) //表示请求中可以携带cookie
.maxAge(3600) //表示预检请求的有效期,有效期内不会在发送第二次预检请求。
.allowedHeaders("*"); //表示允许所有请求头
}
3)使用@CrossOrigin注解:更细粒度的跨域访问控制方式
注:可用在controller上或方法上,会覆盖全局的跨域配置
@Controller
@RequestMapping("/home")
@CrossOrigin(origins = "*", maxAge = 3600)
public class HomeController {
@RequestMapping("/index")
public String index(){
return "index";
}
}
以上四种方式,根据实际项目中拦截器的使用情况灵活选用,但还是最推荐filter的方式,因为最强大和通用。
4、相关知识
1)简单请求和非简单请求
浏览器将跨域请求分为简单请求和非简单请求。只要同时满足以下两大条件,就属于简单请求:
(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
以上任何一个条件不满足的,就属于非简单请求。非简单请求就会额外发送"预检请求"。
2)跨域及产生的原因
跨域的定义:即通讯协议、ip、端口三个有任何一个不相同就被称为跨域。如http://localhost:9000/a.html,要去访问http://localhost:10000/api/getUser接口,就称之为跨域请求,因为两者的端口不一样。此时如果不做处理的话,两者是无法进行通信的。
跨域产生的原因:就是浏览器的同源策略。该策略是在1995年由网景公司引入的,目前各浏览器都支持该策略。该策略的引入也是出于对浏览器访问的一种安全机制,具体例子这里就不细说了,大家去问度娘吧。
3)标准的解决的方案Cors
Cors是W3C组织引入的,全称为(Cross-origin resource sharing)跨域资源共享,是用来解决浏览器跨域访问问题的一种标准的行业规范,目前各大浏览器都支持Cors。
Cors的核心思想是:通过服务器端增加一系列的允许跨域的响应头,来解决跨域问题。
5、结束语
好了,今天就到这里吧,能把以上部分搞明白,日常开发中的跨域问题基本上都能解决了。
最后说一下我的心声:
在我遇到本文开头的那个问题之前,我以为我已经能解决跨域问题了,直到解决那个问题之后,我才发现我所知道的也只是浮于表面的一些东西,更深入的东西还会有,只是我现在还没碰到而已。所以不要自满,不要自以为我最牛逼,别人都是傻逼,每个人都要他所擅长的地方,只是你还没发现而已。
你知道的越多,你就知道的越少,这句话至少在计算机领域是很经典的。