网站主页模板,装潢设计网站,虚拟机建设网站,网站外网怎么做点击上方 IT牧场 #xff0c;选择 置顶或者星标技术干货每日送达作者#xff1a;欧阳我去链接#xff1a;https://segmentfault.com/a/1190000019485883作为一个后端开发#xff0c;我们经常遇到的一个问题就是需要配置 CORS#xff0c;好让我们的前端能够访问到我们的 AP… 点击上方 IT牧场 选择 置顶或者星标技术干货每日送达作者欧阳我去链接https://segmentfault.com/a/1190000019485883作为一个后端开发我们经常遇到的一个问题就是需要配置 CORS好让我们的前端能够访问到我们的 API并且不让其他人访问。而在 Spring 中我们见过很多种 CORS 的配置很多资料都只是告诉我们可以这样配置、可以那样配置但是这些配置有什么区别1、CORS 是什么首先我们要明确CORS 是什么以及规范是如何要求的。这里只是梳理一下流程。CORS 全称是 Cross-Origin Resource Sharing直译过来就是跨域资源共享。要理解这个概念就需要知道域、资源和同源策略这三个概念。域指的是一个站点由 protocal、host 和 port 三部分组成其中 host 可以是域名也可以是 ip port 如果没有指明则是使用 protocal 的默认端口.资源是指一个 URL 对应的内容可以是一张图片、一种字体、一段 HTML 代码、一份 JSON 数据等等任何形式的任何内容。同源策略指的是为了防止 XSS浏览器、客户端应该仅请求与当前页面来自同一个域的资源请求其他域的资源需要通过验证。了解了这三个概念我们就能理解为什么有 CORS 规范了从站点 A 请求站点 B 的资源的时候由于浏览器的同源策略的影响这样的跨域请求将被禁止发送为了让跨域请求能够正常发送我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送这样的机制就是 CORS。2、预检请求在 CORS 中定义了一种预检请求即 preflight request当实际请求不是一个 简单请求 时会发起一次预检请求。预检请求是针对实际请求的 URL 发起一次 OPTIONS 请求并带上下面三个 headers Origin值为当前页面所在的域用于告诉服务器当前请求的域。如果没有这个 header服务器将不会进行 CORS 验证。Access-Control-Request-Method值为实际请求将会使用的方法。Access-Control-Request-Headers值为实际请求将会使用的 header 集合。如果服务器端 CORS 验证失败则会返回客户端错误即 4xx 的状态码。否则将会请求成功返回 200 的状态码并带上下面这些 headersAccess-Control-Allow-Origin允许请求的域多数情况下就是预检请求中的 Origin 的值。Access-Control-Allow-Credentials一个布尔值表示服务器是否允许使用 cookies。Access-Control-Expose-Headers实际请求中可以出现在响应中的 headers 集合。Access-Control-Max-Age预检请求返回的规则可以被缓存的最长时间超过这个时间需要再次发起预检请求。Access-Control-Allow-Methods实际请求中可以使用到的方法集合浏览器会根据预检请求的响应来决定是否发起实际请求。2.1 小结到这里 我们就知道了跨域请求会经历的故事访问另一个域的资源。有可能会发起一次预检请求(非简单请求或超过了 Max-Age)。发起实际请求。接下来我们看看在 Spring 中我们是如何让 CORS 机制在我们的应用中生效的。3、三种配置的方式Spring 提供了多种配置 CORS 的方式有的方式针对单个 API有的方式可以针对整个应用有的方式在一些情况下是等效的而在另一些情况下却又出现不同。我们这里例举几种典型的方式来看看应该如何配置。假设我们有一个 APIRestControllerclass HelloController { GetMapping(hello) fun hello(): String { return Hello, CORS! }}3.1 CrossOrigin 注解使用 CorssOrigin 注解需要引入 Spring Web 的依赖该注解可以作用于方法或者类可以针对这个方法或类对应的一个或多个 API 配置 CORS 规则RestControllerclass HelloController { GetMapping(hello) CrossOrigin(origins [http://localhost:8080]) fun hello(): String { return Hello, CORS! }}3.2 实现 WebMvcConfigurer.addCorsMappings 方法WebMvcConfigurer 是一个接口它同样来自于 Spring Web。我们可以通过实现它的 addCorsMappings 方法来针对全局 API 配置 CORS 规则ConfigurationEnableWebMvcclass MvcConfig: WebMvcConfigurer { override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping(/hello) .allowedOrigins(http://localhost:8080) }}3.3 注入 CorsFilterCorsFilter 同样来自于 Spring Web但是实现 WebMvcConfigurer.addCorsMappings 方法并不会使用到这个类具体原因我们后面来分析。我们可以通过注入一个 CorsFilter 来使用它Configurationclass CORSConfiguration { Bean fun corsFilter(): CorsFilter { val configuration CorsConfiguration() configuration.allowedOrigins listOf(http://localhost:8080) val source UrlBasedCorsConfigurationSource() source.registerCorsConfiguration(/hello, configuration) return CorsFilter(source) }}注入 CorsFilter 不止这一种方式我们还可以通过注入一个 FilterRegistrationBean 来实现这里就不给例子了。在仅仅引入 Spring Web 的情况下实现 WebMvcConfigurer.addCorsMappings 方法和注入 CorsFilter 这两种方式可以达到同样的效果二选一即可。它们的区别会在引入 Spring Security 之后会展现出来我们后面再来分析。4、Spring Security 中的配置在引入了 Spring Security 之后我们会发现前面的方法都不能正确的配置 CORS每次 preflight request 都会得到一个 401 的状态码表示请求没有被授权。这时我们需要增加一点配置才能让 CORS 正常工作Configurationclass SecurityConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity?) { http?.cors() }}或者干脆不实现 WebMvcConfigurer.addCorsMappings 方法或者注入 CorsFilter 而是注入一个 CorsConfigurationSource 同样能与上面的代码配合正确的配置 CORSBeanfun corsConfigurationSource(): CorsConfigurationSource { val configuration CorsConfiguration() configuration.allowedOrigins listOf(http://localhost:8080) val source UrlBasedCorsConfigurationSource() source.registerCorsConfiguration(/hello, configuration) return source}到此我们已经看过了几种典型的例子了我们接下来看看 Spring 到底是如何实现 CORS 验证的。5、这些配置有什么区别我们会主要分析实现 WebMvcConfigurer.addCorsMappings 方法和调用 HttpSecurity.cors 方法这两种方式是如何实现 CORS 的但在进行之前我们要先复习一下 Filter 与 Interceptor 的概念。5.1 Filter 与 Interceptor上图很形象的说明了 Filter 与 Interceptor 的区别一个作用在 DispatcherServlet 调用前一个作用在调用后。但实际上它们本身并没有任何关系是完全独立的概念。Filter 由 Servlet 标准定义要求 Filter 需要在 Servlet 被调用之前调用作用顾名思义就是用来过滤请求。在 Spring Web 应用中DispatcherServlet 就是唯一的 Servlet 实现。Interceptor 由 Spring 自己定义由 DispatcherServlet 调用可以定义在 Handler 调用前后的行为。这里的 Handler 在多数情况下就是我们的 Controller 中对应的方法。对于 Filter 和 Interceptor 的复习就到这里我们只需要知道它们会在什么时候被调用到就能理解后面的内容了。5.2 WebMvcConfigurer.addCorsMappings 方法做了什么我们从 WebMvcConfigurer.addCorsMappings 方法的参数开始先看看 CORS 配置是如何保存到 Spring 上下文中的然后在了解一下 Spring 是如何使用的它们。5.2.1 注入 CORS 配置5.2.1.1 CorsRegistry 和 CorsRegistrationWebMvcConfigurer.addCorsMappings 方法的参数 CorsRegistry 用于注册 CORS 配置它的源码如下public class CorsRegistry { private final List registrations new ArrayList();public CorsRegistration addMapping(String pathPattern) { CorsRegistration registration new CorsRegistration(pathPattern);this.registrations.add(registration);return registration; }protected Map getCorsConfigurations() { Map configs new LinkedHashMap(this.registrations.size());for (CorsRegistration registration : this.registrations) { configs.put(registration.getPathPattern(), registration.getCorsConfiguration()); }return configs; }}我们发现这个类仅仅有两个方法addMapping 接收一个 pathPattern创建一个 CorsRegistration 实例保存到列表后将其返回。在我们的代码中这里的 pathPattern 就是 /hello。getCorsConfigurations 方法将保存的 CORS 规则转换成 Map 后返回。CorsRegistration 这个类同样很简单我们看看它的部分源码public class CorsRegistration { private final String pathPattern; private final CorsConfiguration config; public CorsRegistration(String pathPattern) { this.pathPattern pathPattern; this.config new CorsConfiguration().applyPermitDefaultValues(); } public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); return this; }}不难发现这个类仅仅保存了一个 pathPattern 字符串和 CorsConfiguration很好理解它保存的是一个 pathPattern 对应的 CORS 规则。在它的构造函数中调用的 CorsConfiguration.applyPermitDefaultValues 方法则用于配置默认的 CORS 规则allowedOrigins 默认为所有域。allowedMethods 默认为 GET 、HEAD 和 POST。allowedHeaders 默认为所有。maxAge 默认为 30 分钟。exposedHeaders 默认为 null也就是不暴露任何 header。credentials 默认为 null。创建 CorsRegistration 后我们可以通过它的 allowedOrigins、allowedMethods 等方法修改它的 CorsConfiguration覆盖掉上面的默认值。现在我们已经通过 WebMvcConfigurer.addCorsMappings 方法配置好 CorsRegistry 了接下来看看这些配置会在什么地方被注入到 Spring 上下文中。5.2.1.2 WebMvcConfigurationSupportCorsRegistry.getCorsConfigurations 方法会被 WebMvcConfigurationSupport.getConfigurations 方法调用这个方法如下protected final Map getCorsConfigurations() { if (this.corsConfigurations null) { CorsRegistry registry new CorsRegistry(); addCorsMappings(registry); this.corsConfigurations registry.getCorsConfigurations(); } return this.corsConfigurations;}addCorsMappings(registry) 调用的是自己的方法由子类 DelegatingWebMvcConfiguration 通过委托的方式调用到 WebMvcConfigurer.addCorsMappings 方法我们的配置也由此被读取到。getCorsConfigurations 是一个 protected 方法是为了在扩展该类时仍然能够直接获取到 CORS 配置。而这个方法在这个类里被四个地方调用到这四个调用的地方都是为了注册一个 HandlerMapping 到 Spring 容器中。每一个地方都会调用 mapping.setCorsConfigurations 方法来接收 CORS 配置而这个 setCorsConfigurations 方法则由 AbstractHandlerMapping 提供CorsConfigurations 也被保存在这个抽象类中。到此我们的 CORS 配置借由 AbstractHandlerMapping 被注入到了多个 HandlerMapping 中而这些 HandlerMapping 以 Spring 组件的形式被注册到了 Spring 容器中当请求来临时将会被调用。5.2.2 获取 CORS 配置还记得前面关于 Filter 和 Interceptor 那张图吗当请求来到 Spring Web 时一定会到达 DispatcherServlet 这个唯一的 Servlet。在 DispatcherServlet.doDispatch 方法中会调用所有 HandlerMapping.getHandler 方法。好巧不巧这个方法又是由 AbstractHandlerMapping 实现的OverrideNullablepublic final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // 省略代码 if (CorsUtils.isCorsRequest(request)) { CorsConfiguration globalConfig this.corsConfigurationSource.getCorsConfiguration(request); CorsConfiguration handlerConfig getCorsConfiguration(handler, request); CorsConfiguration config (globalConfig ! null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain;}在这个方法中关于 CORS 的部分都在这个 if 中。我们来看看最后这个 getCorsHandlerExecutionChain 做了什么protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, Nullable CorsConfiguration config) { if (CorsUtils.isPreFlightRequest(request)) { HandlerInterceptor[] interceptors chain.getInterceptors(); chain new HandlerExecutionChain(new PreFlightHandler(config), interceptors); } else { chain.addInterceptor(new CorsInterceptor(config)); } return chain;}可以看到针对 preflight request由于不会有对应的 Handler 来处理所以这里就创建了一个 PreFlightHandler 来作为这次请求的 handler。对于其他的跨域请求因为会有对应的 handler所以就在 handlerExecutionChain 中加入一个 CorsInterceptor 来进行 CORS 验证这里的 PreFlightHandler 和 CorsInterceptor 都是 AbstractHandlerMapping 的内部类实现几乎一致区别仅仅在于一个是 HttpRequestHandler一个是 HandlerInterceptor它们对 CORS 规则的验证都交由 CorsProcessor 接口完成这里采用了默认实现 DefaultCorsProcessor。DefaultCorsProcessor 则是依照 CORS 标准来实现并在验证失败的时候打印 debug 日志并拒绝请求。我们只需要关注一下标准中没有定义的验证失败时的状态码protected void rejectRequest(ServerHttpResponse response) throws IOException { response.setStatusCode(HttpStatus.FORBIDDEN); response.getBody().write(Invalid CORS request.getBytes(StandardCharsets.UTF_8));}CORS 验证失败时调用这个方法并设置状态码为 403。5.2.3 小结通过对源码的研究我们发现实现 WebMvcConfigurer.addCorsMappings 方法的方式配置 CORS会在 Interceptor 或者 Handler 层进行 CORS 验证。5.3 HtttpSecurity.cors 方法做了什么在研究这个方法的行为之前我们先来回想一下我们调用这个方法解决的是什么问题。前面我们通过某种方式配置好 CORS 后引入 Spring SecurityCORS 就失效了直到调用这个方法后CORS 规则才重新生效。下面这些原因导致了 preflight request 无法通过身份验证从而导致 CORS 失效preflight request 不会携带认证信息。Spring Security 通过 Filter 来进行身份验证。Interceptor 和 HttpRequestHanlder 在 DispatcherServlet 之后被调用。Spring Security 中的 Filter 优先级比我们注入的 CorsFilter 优先级高。接下来我们就来看看 HttpSecurity.cors 方法是如何解决这个问题的。5.3.1 CorsConfigurer 如何配置 CORS 规则HttpSecurity.cors 方法中其实只有一行代码public CorsConfigurer cors() throws Exception { return getOrApply(new CorsConfigurer());}这里调用的 getOrApply 方法会将 SecurityConfigurerAdapter 的子类实例加入到它的父类 AbstractConfiguredSecurityBuilder 维护的一个 Map 中然后一个个的调用 configure 方法。所以我们来关注一下 CorsConfigurer.configure 方法就好了。Overridepublic void configure(H http) throws Exception { ApplicationContext context http.getSharedObject(ApplicationContext.class); CorsFilter corsFilter getCorsFilter(context); if (corsFilter null) { throw new IllegalStateException( Please configure either a CORS_FILTER_BEAN_NAME bean or a CORS_CONFIGURATION_SOURCE_BEAN_NAME bean.); } http.addFilter(corsFilter);}这段代码很好理解就是在当前的 Spring Context 中找到一个 CorsFilter然后将它加入到 http 对象的 filters 中。由上面的 HttpSecurity.cors 方法可知这里的 http 对象实际类型就是 HttpSecurity。5.3.2 getCorsFilter 方法做了什么也许你会好奇HttpSecurity 要如何保证 CorsFilter 一定在 Spring Security 的 Filters 之前调用。但是在研究这个之前我们先来看看同样重要的 getCorsFilter 方法这里可以解答我们前面的一些疑问。private CorsFilter getCorsFilter(ApplicationContext context) { if (this.configurationSource ! null) { return new CorsFilter(this.configurationSource); } boolean containsCorsFilter context .containsBeanDefinition(CORS_FILTER_BEAN_NAME); if (containsCorsFilter) { return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class); } boolean containsCorsSource context .containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME); if (containsCorsSource) { CorsConfigurationSource configurationSource context.getBean( CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class); return new CorsFilter(configurationSource); } boolean mvcPresent ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, context.getClassLoader()); if (mvcPresent) { return MvcCorsFilter.getMvcCorsFilter(context); } return null;}这是 CorsConfigurer 寻找 CorsFilter 的全部逻辑我们用人话来说就是CorsConfigurer 自己是否有配置 CorsConfigurationSource如果有的话就用它创建一个 CorsFilter。在当前的上下文中是否存在一个名为 corsFilter 的实例如果有的话就把他当作一个 CorsFilter 来用。在当前的上下文中是否存在一个名为 corsConfigurationSource 的 CorsConfigurationSource 实例如果有的话就用它创建一个 CorsFilter。在当前上下文的类加载器中是否存在类 HandlerMappingIntrospector如果有的话则通过 MvcCorsFilter 这个内部类创建一个 CorsFilter。如果没有找到那就返回一个 null调用的地方最后会抛出异常阻止 Spring 初始化。上面的第 2、3、4 步能解答我们前面的配置为什么生效以及它们的区别。注册 CorsFilter 的方式这个 Filter 最终会被直接注册到 Servlet container 中被使用到。注册 CorsConfigurationSource 的方式会用这个 source 创建一个 CorsFiltet 然后注册到 Servlet container 中被使用到。而第四步的情况比较复杂。HandlerMappingIntrospector 是 Spring Web 提供的一个类实现了 CorsConfigurationSource 接口所以在 MvcCorsFilter 中它被直接用于创建 CorsFilter。它实现的 getCorsConfiguration 方法会经历遍历 HandlerMapping。调用 getHandler 方法得到 HandlerExecutionChain。从中找到 CorsConfigurationSource 的实例。调用这个实例的 getCorsConfiguration 方法返回得到的 CorsConfiguration。所以得到的 CorsConfigurationSource 实例实际上就是前面讲到的 CorsInterceptor 或者 PreFlightHandler。所以第四步实际上匹配的是实现 WebMvcConfigurer.addCorsMappings 方法的方式。由于在 CorsFilter 中每次处理请求时都会调用 CorsConfigurationSource.getCorsConfiguration 方法而 DispatcherServlet 中也会每次调用 HandlerMapping.getHandler 方法再加上这时的 HandlerExecutionChain 中还有 CorsInterceptor所以使用这个方式相对于其他方式做了很多重复的工作。所以 WebMvcConfigurer.addCorsMappings HttpSecurity.cors 的方式降低了我们代码的效率也许微乎其微但能避免的情况下还是不要使用。5.3.3 HttpSecurity 中的 filters 属性在 CorsConfigurer.configure 方法中调用的 HttpSecurity.addFilter 方法由它的父类 HttpSecurityBuilder 声明并约定了很多 Filter 的顺序。然而 CorsFilter 并不在其中。不过在 Spring Security 中目前还只有 HttpSecurity 这一个实现所以我们来看看这里的代码实现就知道 CorsFilter 会排在什么地方了。public HttpSecurity addFilter(Filter filter) { Class extends Filter filterClass filter.getClass(); if (!comparator.isRegistered(filterClass)) { throw new IllegalArgumentException(...); } this.filters.add(filter); return this;}我们可以看到Filter 会被直接加到 List 中而不是按照一定的顺序来加入的。但同时我们也发现了一个 comparator 对象并且只有被注册到了该类的 Filter 才能被加入到 filters 属性中。这个 comparator 又是用来做什么的呢在 Spring Security 创建过程中会调用到 HttpSeciryt.performBuild 方法在这里我们可以看到 filters 和 comparator 是如何被使用到的。protected DefaultSecurityFilterChain performBuild() throws Exception { Collections.sort(filters, comparator); return new DefaultSecurityFilterChain(requestMatcher, filters);}可以看到Spring Security 使用了这个 comparator 在获取 SecurityFilterChain 的时候来保证 filters 的顺序所以研究这个 comparator 就能知道在 SecurityFilterChain 中的那些 Filter 的顺序是如何的了。这个 comparator 的类型是 FilterComparator 从名字就能看出来是专用于 Filter 比较的类它的实现也并不神秘从构造函数就能猜到是如何实现的FilterComparator() { Step order new Step(INITIAL_ORDER, ORDER_STEP); put(ChannelProcessingFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); put(WebAsyncManagerIntegrationFilter.class, order.next()); put(SecurityContextPersistenceFilter.class, order.next()); put(HeaderWriterFilter.class, order.next()); put(CorsFilter.class, order.next()); // 省略代码}可以看到 CorsFilter 排在了第六位在所有的 Security Filter 之前由此便解决了 preflight request 没有携带认证信息的问题。5.3.4 小结引入 Spring Security 之后我们的 CORS 验证实际上是依然运行着的只是因为 preflight request 不会携带认证信息所以无法通过身份验证。使用 HttpSecurity.cors 方法会帮助我们在当前的 Spring Context 中找到或创建一个 CorsFilter 并安排在身份验证的 Filter 之前以保证能对 preflight request 正确处理。6、总结研究了 Spring 中 CORS 的代码我们了解到了这样一些知识实现 WebMvcConfigurer.addCorsMappings 方法来进行的 CORS 配置最后会在 Spring 的 Interceptor 或 Handler 中生效。注入 CorsFilter 的方式会让 CORS 验证在 Filter 中生效。引入 Spring Security 后需要调用 HttpSecurity.cors 方法以保证 CorsFilter 会在身份验证相关的 Filter 之前执行。HttpSecurity.cors WebMvcConfigurer.addCorsMappings 是一种相对低效的方式会导致跨域请求分别在 Filter 和 Interceptor 层各经历一次 CORS 验证。HttpSecurity.cors 注册 CorsFilter 与 HttpSecurity.cors 注册 CorsConfigurationSource 在运行的时候是等效的。在 Spring 中没有通过 CORS 验证的请求会得到状态码为 403 的响应。干货分享最近将个人学习笔记整理成册使用PDF分享。关注我回复如下代码即可获得百度盘地址无套路领取•001《Java并发与高并发解决方案》学习笔记•002《深入JVM内核——原理、诊断与优化》学习笔记•003《Java面试宝典》•004《Docker开源书》•005《Kubernetes开源书》•006《DDD速成(领域驱动设计速成)》•007全部•008加技术群讨论近期热文•LinkedBlockingQueue vs ConcurrentLinkedQueue•解读Java 8 中为并发而生的 ConcurrentHashMap•Redis性能监控指标汇总•最全的DevOps工具集合再也不怕选型了•微服务架构下解决数据库跨库查询的一些思路•聊聊大厂面试官必问的 MySQL 锁机制关注我喜欢就点个在看呗^_^