Spring MVC处理 CORS 跨域

跨源资源共享(CORS) 即 Cross-Origin Resource Sharing,也常被译为跨域资源共享。作为 W3C 的标准,它允许浏览器向跨源服务器发起请求,克服了 AJAX 只能同源使用的限制

CORS 需要浏览器和服务器同时支持,浏览器发起跨域请求时会自动携带一些请求头,服务器如果允许跨域,也会自动添加一些响应头。作为运行在服务端的 Spring MVC 也对 CORS 提供了支持,并提供了多种解决方案。

浏览器同源策略

同源策略是 Netscape 公司在 1995 年引入浏览器的,目前所有浏览器都遵循了同源策略。

同源表示两个网页的 协议、域名、端口号 三者都一致。同源策略最初的目的是为了保护 A 网页设置的 cookie 在 B 网页中不能读取。

设想如果用户同时打开了银行网站和钓鱼网站,如果没有同源策略,钓鱼网站拿到银行网站的 cookie 后发起转账或者读取用户敏感信息,将会很危险。

非同源的网页,对 Cookie、LocalStorage、IndexDB 读取、Dom 文档获取、Ajax 请求有着严格的限制。同源策略规定了只能向同源的网址发起 AJAX 请求。CORS 正是解决跨域 AJAX 的标准方案,相比使用 JSONP 方案来说更为灵活。

CORS 处理流程

浏览器的请求可以分为简单请求和非简单请求两种。浏览器对不同类型的请求进行 CORS 处理的方式有所不同。

简单请求

满足下面三个条件的请求可以被称为简单请求:

  • 请求方法为 GET、HEAD、POST 之一。
  • 允许设置的请求头包括 Accept、Accept-Language、Content-Language、Content-Type。
  • Content-Type 的值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded。

简单请求流程如下图所示:

在这里插入图片描述

浏览器发起请求时在请求头添加 Origin 字段,表示当前资源所在的源,服务端收到请求后检查该字段,如果允许该请求则在响应头添加Access-Control-Allow-Origin 字段,否则可以拒绝处理请求并返回错误的 HTTP 响应码,浏览器收到响应后发现没有 Access-Control-Allow-Origin 字段或者 Access-Control-Allow-Origin 字段值有误则会在控制台打印不允许跨域的错误信息。

非简单请求

对于非简单请求,浏览器首先会发起预请求 Preflight Request 检查是否允许跨域,如果允许跨域才会执行真正的请求,流程如下所示:

在这里插入图片描述

预请求的 HTTP 方法为 OPTIONS,携带的请求头如下:

  • Origin:表示资源所在的源。
  • Access-Control-Request-Method:表示真实 HTTP 请求方法的请求头。
  • Access-Control-Request-Headers:表示真实 HTTP 请求方法自定义的请求头 。

如果服务端允许跨域请求,会在响应头添加如下的字段:

  • Access-Control-Allow-Origin :表示跨域请求允许的源,* 表示允许任何源。
  • Access-Control-Request-Method:表示跨域请求允许使用的请求方法,可以比请求头中的多。
  • Access-Control-Request-Headers:表示跨域请求允许携带的请求头。

如果服务端不允许跨域请求,则可以直接返回表示错误的 HTTP 响应码。

浏览器收到预请求响应后检查响应头判断是否允许跨域,如果不允许跨域则直接在控制台打印跨域报错信息。如果允许跨域再正常发起请求,携带请求头 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 以及自定义的请求头。

携带用户身份的跨域请求

服务端检查跨域请求后,除了返回基本的响应头,还可以添加如下额外的响应头:

  • Access-Control-Max-Age:表示跨域检查结果在浏览器中可以缓存的秒数。
  • Access-Control-Expose-Headers:默认情况 JS 只能获取一些基本的响应头,这个字段允许 JS 可以获取除基本响应头的其他响应头。

除此之外,服务端还可以返回值为 true 的响应头 Access-Control-Allow-Credentials,这个响应头可以让浏览器在跨域请求时携带 Cookie 信息,当然了,需要在发起请求时配置 withCredentials=true。

在这里插入图片描述

如果服务端返回了响应头 Access-Control-Allow-Credentials,此时 Access-Control-Allow-Origin 不能返回 *,否则请求将会失败。

Spring MVC CORS 处理

由于每个接口都需要处理跨域请求,因此在传统的 Java Web 项目中通常使用 Filter 进行全局处理。

Spring MVC 中进行跨域处理的核心类是 HandlerMapping,当请求到达 DispatchServlet,如果请求是预请求 Spring 会将处理器替换为跨域处理器,如果请求是非预请求 Spring 将在拦截器链前面添加跨域拦截器,然后根据 CORS 配置进行相应的处理。再把 DispatcherServlet 流程图祭出,和 CORS 相关的部分可以见右上角:

在这里插入图片描述

CorsFilter

Filter 是解决跨域的传统方式,Spring 出现前,我们经常会写一个解决跨域的 Filter,当请求到来时向响应头中添加固定的字段。

Spring MVC 提供了一个具有相同功能的 CorsFilter,这样以后我们就不需要每个项目都单独写一个处理跨域的 Filter 了。

SpringBoot 环境下配置 CorsFilter 示例如下:

@Configuration
public class WebMvcConfig {

    @Bean
    public Filter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("http://hukp.cn");
        corsConfiguration.addAllowedMethod(HttpMethod.POST);
        corsConfiguration.addAllowedHeader("token");
        corsConfiguration.setExposedHeaders(Arrays.asList("header1", "header2"));
        corsConfiguration.setMaxAge(3600L);
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/*", corsConfiguration);
        CorsFilter corsFilter = new CorsFilter(corsConfigurationSource);
        return corsFilter;
    }

}

实例化 CorsFilter 时需要指定一个 CorsConfigurationSource 实例用来获取跨域配置 CorsConfiguration,常用的实现是 UrlBasedCorsConfigurationSource。

全局 CORS 配置

Spring MVC 官方解决 CORS 的做法是在 HandlerMappping 获取处理器链时根据是否为预请求使用 PreFlightHandler 作为处理器或者添加拦截器 CorsInterceptor,具体可以参见源码AbstractHandlerMapping#getCorsHandlerExecutionChain。

对于用户而言,只需要进行 CORS 配置就可以了,而配置分为全局配置和局部配置,Spring 会把这两个配置进行合并。对于全局配置而言有 API 和 XML 两种配置方式。

XML 配置

XML 配置是 Spring 早期提供的支持,和上述 CorsFilter 等价的 CORS 配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <mvc:cors>
        <mvc:mapping path="/*"
                     allowed-origins="http://hukp.com"
                     allowed-methods="POST"
                     allowed-headers="token"
                     exposed-headers="header1, header2"
                     max-age="3600"
                     allow-credentials="true"
        />
    </mvc:cors>
</beans>

API 配置

当前注解已经成为 Spring 的主流使用方式,使用 @EnableWebMvc 开启 Web 相关特性后可以通过实现接口 WebMvcConfiger 进行跨域配置,最终这个配置将传递到 AbstractHandlerMapping。和 XML 等价的 API 配置方式如下:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/*")
                .allowedOrigins("http://hukp.cn")
                .allowedMethods("POST")
                .allowedHeaders("token")
                .exposedHeaders("header1", "header2")
                .maxAge(3600)
                .allowCredentials(true);
    }
}

局部 CORS 配置

除了全局配置,Spring 还可以针对每个处理器做特殊的配置。

API 配置

如果想用一个处理器类处理一个请求,这个处理器类可以实现接口 HttpRequestHandler、Controller 或者 HandlerFunction,如果想要为这个处理器进行 CORS 处理,还需要实现接口 CorsConfigurationSource。以登录场景为例,示例代码如下:

public class LoginHandler implements Controller, CorsConfigurationSource {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 省略相关逻辑
        return new ModelAndView();
    }

    // 获取 CORS 配置
    @Override
    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("http://hukp.cn");
        corsConfiguration.addAllowedMethod(HttpMethod.POST);
        corsConfiguration.addAllowedHeader("token");
        corsConfiguration.setExposedHeaders(Arrays.asList("header1", "header2"));
        corsConfiguration.setMaxAge(3600L);
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }
}

注解配置

Spring 添加对注解的支持后,我们使用 @Controller 标注控制器类,然后在类中使用 @RequestMapping 标注处理器方法。针对这种方式,由于请求由方法进行处理,我们没办法实现 CorsConfigurationSource 做跨域配置,但是 Spring 也提供了对应的解决方案。

我们可以使用 @CrossOrigin 注解做跨域配置,可以把这个类加在控制器类或者控制器方法上,控制器类上的 @CrossOrigin 适用于所有的控制器方法,控制器方法上的 @CrossOrigin 适用于自身,如果类和方法上都有 @CrossOrigin 注解,Spring 则会将配置合并。

示例代码如下:

@Controller
@CrossOrigin(origins = "http://hukp.cn",
        allowedHeaders = "token")
public class LoginController {

    @CrossOrigin(methods = RequestMethod.POST,
            exposedHeaders = {"header1", "header2"},
            maxAge = 3600L,
            allowCredentials = "true")
    @PostMapping("/login")
    public ModelAndView login(HttpServletRequest request) {
        // 省略业务逻辑
        return new ModelAndView();
    }
}

当请求 /login 到达时,将使用类和方法上合并后的 CORS 配置。

Spring Security CORS 处理

Spring Boot 环境下如果引入了 Spring Security,Spring 将自动配置 CorsFilter,此时从CorsConfigurationSource 类型的 bean 中读取 CORS 配置,因此将 CorsConfigurationSource 配置为 bean 即可。示例代码如下:

@Configuration
public class WebMvcConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("http://hukp.cn");
        corsConfiguration.addAllowedMethod(HttpMethod.POST);
        corsConfiguration.addAllowedHeader("token");
        corsConfiguration.setExposedHeaders(Arrays.asList("header1", "header2"));
        corsConfiguration.setMaxAge(3600L);
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/*", corsConfiguration);
        return corsConfigurationSource;
    }
}

 

参考:

 

posted @ 2022-01-11 23:10  残城碎梦  阅读(858)  评论(0编辑  收藏  举报