从 CORS 到 Spring MVC
跨源资源共享(CORS) 即 Cross-Origin Resource Sharing,也常被译为跨域资源共享。作为 W3C 的标准,它允许浏览器向跨源服务器发起请求,克服了 AJAX 只能同源使用的限制。
CORS 需要浏览器和服务器同时支持,浏览器发起跨域请求时会自动携带一些请求头,服务器如果允许跨域,也会自动添加一些响应头。作为运行在服务端的 Spring MVC 也对 CORS 提供了支持,并提供了多种解决方案。
认识 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 了。Spring MVC 中配置 Filter 的方式可以参见【Spring MVC 系列】Spring MVC 中 Filter 配置的 6 种方式,看看你了解哪些。
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; } }
总结
这篇主要对 CORS 规范进行了简单的介绍,并全面的介绍了 Spring MVC 中进行 CORS 配置的几种方式,希望大家在学习技术的时候也尝试了解其背后的内容,做到知其然,也要知其所以然。如果有问题,欢迎留言。