转载请标明出处:
https://blog.csdn.net/bingospunky/article/details/80136164
本文出自马彬彬的博客
现象
项目中前后端分离部署,所以需要解决跨域的问题。
我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。
当用户登录以后,正常使用;当用户退出登录状态时,出现了跨域的现象。
跨域
我们处理跨域的方式是继承WebMvcConfigurerAdapter,添加Cors的支持,代码如下:
Code1
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH").allowCredentials(true).maxAge(3600);
}
}
用户权限
我们把用户登录的信息存放在cookie里,使用拦截器+注解的方式处理
当用户访问一个controller的方法时,会在拦截器里获取该方法的注解,通过注解判断该方法是否需要登录才能访问。如果需要登录才能访问的接口,获取cookie里的值,判断用户是否登录了,如果该用户没有登录,说明他没有访问该接口的权限,那么直接返回需要登录的信息给用户。代码如下:
Code2
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptorAdapter() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod mHandler = (HandlerMethod) handler;
String token = CookieUtil.getCookieValue(request, Cookies.USER_COOKIE);
//如果有用户cookie,尝试获取用户信息
ItBaseUser user = tryToLoadAccount(mHandler, request, token);
//如果要求做登录校验,检查信息是否正确
if (mHandler.hasMethodAnnotation(LoginRequired.class) && user == null) {
if (logger.isDebugEnabled())
logger.debug("[addCheckLoginInterceptor] Login required function without login message, token -> {}, url -> {}",
token, request.getRequestURI());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
Response resp = BaseController.statusResponse(ResponseCode.NOT_LOGIN);
response.getOutputStream().write(GsonUtils.format(resp).getBytes());
return false;
}
}
return true; //always true
}
});
}
}
1.对于已经登录的用户,可以正常的使用。2.对于那么不需要登录的接口,也可以正常使用。3.对于需要登录的接口,如果用户没有登录,应该在Code2第19、20行进行处理,直接返回需要登录的json字符串。但是实际的情况是,浏览器遇到跨域问题。
情况1、2都可以使用,没有跨域问题,情况3有跨域问题,这说明是否明跨域和我们的业务相关了。
这是为什么呢?
原因
这里很明显会猜测应该是我们的权限拦截器和处理跨域的方式起了冲突。
cors实现跨域,就是在http的response里添加适当的header来实现的。那这部分功能在哪里呢?答案是在拦截器里。对于我们的代码,一共有4个拦截器,我们的权限拦截器在第一个位置,cors拦截器在第4个位置。当请求遇到Code2第14行代码返回true时,会直接给客户端返回值,并且在Code2第21行返回true。拦截器是责任链模式,第21行返回了true,后面的拦截器将不会再执行,所以cors拦截器不会被执行,header不会被赋值。我们在浏览器里就会出现跨域。
如何解决
调整拦截器顺序
首先想到的方法是改变拦截器的顺序,让cors拦截器在最前面。
很遗憾,这点做不到。
为什么做不到呢?
拦截器有这几种组成。1.我们自定义的拦截器,比如Code2第5行代码所添加的,这里添加的拦截器我们可以控制他们的顺序。2.ConversionServiceExposingInterceptor和ResourceUrlProviderExposingInterceptor,他们的顺序在我们自定义拦截器后面,代码在Code3。3.根据请求是否是cors请求,决定是否添加cors拦截器。代码在Code4。
Code3
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
protected final Object[] getInterceptors() {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
this.addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(this.mvcConversionService()));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(this.mvcResourceUrlProvider()));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
Code3第4行先添加了自定义拦截器,第5、6行添加了那两个拦截器。
Code4
org.springframework.web.servlet.handler.AbstractHandlerMapping
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
chain = new HandlerExecutionChain(new AbstractHandlerMapping.PreFlightHandler(config), interceptors);
} else {
chain.addInterceptor(new AbstractHandlerMapping.CorsInterceptor(config));
}
return chain;
}
当请求是跨域请求时,在构造HandlerExecutionChain的过程中,会添加cors拦截器,cors拦截器在最后。
使用filter来完成cors的工作
我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。代码如下:
Code5
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
}
去掉WebMvcConfigurerAdapter中关于cors的配置。
参考
when interceptor preHandler throw exception, the cors is broken #9595
SpringBoot 实现前后端分离的跨域访问(CORS)