2018-12-18 · Develop

Spring Boot - MVC 特性配置

作为一个 Web 项目,定制化 MVC 特性是必不可少的, Spring Boot 提供了 WebMvcConfigurer 来配置应用的 MVC 全局特性。

WebMvcConfigurer 是一个接口类,定义了如下的接口可供使用:

spring-boot-web-mvc-configurer

跨域访问

出于安全的考虑,浏览器会禁用 AJAX 访问不同域的地址,但是在前后端分离的情况下,跨域又是必须的。 Spring Boot 提供了对 CORS 的支持,可以实现 addCorsMappings 接口来添加跨域配置:

@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
            .allowedOrigins("*")
            .allowCredentials(true)
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD")
            .maxAge(3600);
}

简单点理解跨域就是,浏览器检查 Response 的 HTTP 头信息,如果 Access-Control-Allow-Origin 包含了自身域,这允许访问。否则报错,这就是 allowedOrigins 的作用

类型转化器

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
}

资源处理

比如过滤 swagger-api

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
    registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    registry.addResourceHandler("/swagger/**").addResourceLocations("classpath:/META-INF/resources/swagger/");
    registry.addResourceHandler("/swagger-resources/**").addResourceLocations("classpath:/META-INF/resources/swagger-resources/");
//  registry.addResourceHandler("/v2/api-docs/**").addResourceLocations("classpath:/META-INF/resources/v2/api-docs/");
}

拦截器

其他的还有很多,这里再讲一个,拦截器的使用:

@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MethodHandlerInterceptor());
    }

    class MethodHandlerInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 在 Controller 方法前会调用此方法。
            if (handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                String methodName = handlerMethod.getMethod().getName();
                String beanTypeName = handlerMethod.getBeanType().getName();
                log.info(beanTypeName + "." + methodName + "()");
                // TODO
                return true;
            }
            return false;
        }

        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
            // 在调用 Controller 方法结束后,页面渲染前调用此方法。
        }

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
            // 页面渲染完毕后调用此方法。
        }
    }
}

其中三个接口为:

这个时候需要注意的是,在跨域的情况下, handler 并不能转换成 HandlerMethod ,而是 AbstractHandlerMapping.PreFlightHandler 类。
我们都知道的是使用 Access-Control 的跨域请求有两种,简单的跨域请求和带有先导请求(options)的跨域请求:

简单的跨域请求

简单的跨域请求和带有先导请求的跨域请求最大的区别是他不会先发送一个先导请求。
浏览器对简单的跨域请求的定义是

带有先导请求(options)的跨域请求

带有先导请求(options)的跨域请求,浏览器会自动先发送一个options方法的请求到服务器端,并查看相应头里是否有Access-Control头部。值得注意的是options方法的请求并不会携带cookie,也就是如果如果你的请求必须要登陆验证的话那么你就必须构造一个简单请求。 关于 HTTP 的首部字段请看 HTTP 响应首部字段HTTP 请求首部字段

所以解决的方案就是

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    if (request.getMethod().equals(HttpMethod.OPTIONS.name())) {
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type,Access-Token");
        response.setHeader("Access-Control-Expose-Headers", "*");
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (servletRequestAttributes != null) {
            String origin = servletRequestAttributes.getRequest().getHeader("Origin");
            response.setHeader("Access-Control-Allow-Origin", origin);
        }
        return true;
    }
    if (handler instanceof HandlerMethod) {
        // TODO
    }
}

或者使用 spring 自带的 corsFilter :

@Bean
public FilterRegistrationBean getFilterRegistrationBean() {
    UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.addAllowedOrigin("*");
    corsConfiguration.addAllowedHeader("*");
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.addAllowedMethod("*");
    corsConfiguration.setMaxAge(3600L);
    urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
    CorsFilter corsFilter = new CorsFilter(urlBasedCorsConfigurationSource);
    FilterRegistrationBean<CorsFilter> filterRegistrationBean = new FilterRegistrationBean<>(corsFilter);
    filterRegistrationBean.setOrder(0);
    return filterRegistrationBean;
}

参考文档:
跨域!跨域!
浏览器同源策略以及跨域请求时可能遇到的问题
HTTP访问控制(CORS)