• 11.Spring security跨域问题


    文章目录

    跨域问题

    11.1什么是CORS

    CORS(Cross-Origin Resource Sharing)是由W3C制定的一种跨域资源共享技术标准,其目的就是为了解决前端的跨域请求
    其中新增了一组HTTP请求头字段,通过这些字段,服务器告诉浏览器,哪些网站通过浏览器有权限访问哪些资源。同时规定,对那些可能修改服务器数据的HTTP请求方法(如GET以外的HTTP请求等),浏览器必须首先使用OPTIONS方法发起一个预检请求,预检请求的目的是查看服务端是否支持即将发起的跨域请求,如果服务端允许,才能发起实际的HTTP请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(如cookie、HTTP认证信息等)。

    GET请求为例,如果需要发起一个跨域请求,则请求头如下:

    Host: localhost:8080
    Origin: http://localhost:8081
    Referer: http://localhost:8081/index.html
    
    • 1
    • 2
    • 3

    如果服务端支持该跨域请求,那么返回的响应头中将包含:

    Access-Control-Allow-Origin: http://localhost:8081
    
    • 1

    Access-Control-Allow-Origin字段用来告诉浏览器可以访问该资源的域,当浏览器收到这样的响应头信息之后,提取出Access-Control-Allow-Origin字段中的值,发现该值包含当前页面所在的域,就知道这个跨域是被允许的,因此就不再对前端的跨域请求进行限制。
    这属于简单请求,即不需要进行预检请求的跨域。

    对于一些非简单请求,会首先发送一个预检请求。类似于:

    OPTIONS: /put HTTP/1.1
    Host: localhost:8080
    Connection: keep-alive
    Accept: */*
    Access-Control-Request-Method: PUT
    Origin: http://localhost:8081
    Referer: http://localhost:8081/index.html
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    请求方法是OPTIONS,请求头Origin字段告诉服务端当前页面所在的域,请求头Access-Control-Request-Method告诉服务端即将发起的跨域请求所使用的方法。服务端对此进行判断,如果允许即将发起的跨域请求,则会给出如下响应:

    HTTP/1.1 200
    Access-Control-Allow-Origin: http://localhost:8081
    Access-Control-Allow-Methods: PUT
    Access-Control-Max-Age: 3600
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Access-Control-Allow-Methods字段表示允许的跨域方法;Access-Control-Max-Age字段表示预检请求的有效期,单位为秒,在有效期内如果发起该跨域请求,则不用再次发起预检请求。预检请求结束后,接下来就会发起一个真正的跨域请求。

    11.2Spring处理方案

    11.2.1@CrossOrigin

    Spring中第一种处理跨域的方式是通过@CrossOrigin注解来标记支持跨域,该注解可以添加在方法上,也可以添加在Controller上。当添加在Controller上时,表示所有接口都支持跨域。

    @RestController
    public class HelloController {
        @CrossOrigin(origins = "http://localhost:8081")
        @PostMapping("/post")
        public String post() {
            return "hello post";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    @CrossOrigin注解各属性含义如下:

    • allowCredentials:浏览器是否应当发送凭证信息,如cookie。
    • allowedHeaders:请求被允许的请求头字段,*表示所有字段。
    • exposedHeaders:哪些响应头可以作为响应的一部分暴露出来。注意,这里只可以一一列举,通配符*在这里是无效的。
    • maxAge:预检请求的有效期,有效期内不必再次发送,默认是1800秒。
    • methods:允许的请求方法,*表示允许所有方法。
    • origins:允许的域,*表示允许所有域。

    具体的执行过程:

    1. @CrossOrigin注解在AbstractHandlerMethodMapping的内部类MappingRegistryregister方法中完成解析的,@CrossOrigin注解中的内容会被解析成一个配置对象CorsConfiguration
    2. @CrossOrigin所标记的请求方法对象HandlerMethodCorsConfiguration一一对应存入一个名为corsLookupMap映射中。
    3. 当请求到达DispatcherServlet#doDispatch方法之后,调用AbstractHandlerMapping#getHandler方法获取执行链HandlerExecutionChain时,会从corsLookup映射中获取到CorsConfiguration对象。
    4. 根据获取到的CorsConfiguration对象构建一个CorsInterceptor拦截器。
    5. CorsInterceptor拦截器中触发对DefaultCorsProcessor#processRequest的调用,跨域请求的校验工作将在该方法中完成。
    11.2.2addCorsMappings

    有一种全局的配置方法,通过重写WebMvcConfigurerComposite#addCorsMappings方法来实现:

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            // addMapping表示要处理的请求地址
            registry.addMapping("/**")
                .allowedMethods("*")
                .allowedOrigins("*")
                .allowedHeaders("*")
                .allowCredentials(false)
                .exposedHeaders("")
                .maxAge(3600);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    全局的配置方式最终的处理方式和@CrossOrigin注解相同,都是在CorsInterceptor拦截器中触发对DefaultCorsProcessor#processRequest的调用,并最终在该方法中完成对跨域请求的校验工作,不过在源码执行过程中略有差异。

    1. registry.addMapping("/**")方法配置了一个CorsRegistration对象,该对象中包含了一个路径拦截规则,拦截规则的值就是addMapping方法的参数,同时CorsRegistration中还包含了一个CorsConfiguration配置对象,该对象用来保存这里跨域相关的配置。
    2. WebMvcConfigurationSupport#requestMappingHandlerMapping方法中触发了addCorsMappings方法的执行,将获取到的CorsRegistration对象重新组装成一个UrlBasedCorsConfigurationSource对象,该对象中定义了一个corsConfigurations变量(Map),该变量保存了拦截器规则和CorsConfiguration对象的映射关系。
    3. 将新建的UrlBasedCorsConfigurationSource对象赋值给AbstractHandlerMapping#corsConfigurationSource属性。
    4. 当请求到达时的处理方法和@CrossOrigin注解处理流程的第3步一样,都是在AbstractHandlerMapping#getHandler方法中进行处理,不同的是,这里是从corsConfigurationSource中获取CorsConfiguration配置对象,而@CrossOrigin注解则从corsLookup映射中获取。如果两处都可以获取到,则对获取到的对象属性值进行合并。
    5. 根据获取到的CorsConfiguration对象构建一个CorsInterceptor拦截器。
    6. CorsInterceptor拦截器中触发对DefaultCorsProcessor#processRequest的调用,跨域请求的校验工作将在该方法中完成。

    这两种跨域配置方式殊途同归,最终目的都是配置了一个CorsConfiguration对象,并根据该对象创建CorsInterceptor拦截器,然后在拦截器中触发DefaultCorsProcessor#processRequest方法的执行,完成跨域的校验。
    另外还需要注意的是,这里的跨域校验是由DispatchServlet中的方法触发的,而DispatchServlet的执行是在Filter之后,这一点需要牢记。

    11.2.3CorsFilter

    CorsFilter是spring web中提供的一个处理跨域的过滤器,开发者也可以通过该过滤器处理跨域:

    @Configuration
    public class WebMvcConfig {
        /**
         * 由于是在spring boot项目中,这里通过FilterRegistrationBean来配置一个过滤器,这种配置方式既可以设置拦截规则,
         * 又可以为配置的过滤器设置优先级
         */
        @Bean
        FilterRegistrationBean corsFilter() {
            FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
            // 依然离不开CorsConfiguration对象,不同的是自己手动创建该对象,并逐个设置跨域的各项处理规则
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
            corsConfiguration.setAllowedMethods(Arrays.asList("*"));
            corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
            corsConfiguration.setMaxAge(3600L);
            // 还需要创建一个UrlBasedCorsConfigurationSource对象,将过滤器的拦截规则和CorsConfiguration
            // 对象之间的映射关系由UrlBasedCorsConfigurationSource中的corsConfigurations变量保存起来
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", corsConfiguration);
            // 创建一个CorsFilter,并为其配置一个优先级
            registrationBean.setFilter(new CorsFilter(source));
            registrationBean.setOrder(-1);
            return registrationBean;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    CorsFilter过滤器的doFilterInternal方法中,触发对DefaultCorsProcessor#processRequest的调用,进而完成跨域请求的校验。
    和前面两种方式不同的是,CorsFilter是在过滤器中处理跨域的,而前面两种方案则是在DispatchServlet中触发跨域处理,从处理时间上来说,CorsFilter对于跨域的处理时机要早于前面两种。

    这三种方式选择其中一种即可,不过需要说明的是:

    • @CrossOrigin注解 + 重写addCorsMappings方法同时配置,这两种方式中关于跨域的配置会自动合并,跨域在CorsInterceptor中只处理了一次。
    • @CrossOrigin注解 + CorsFilter同时配置,或者重写addCorsMappings方法 + CorsFilter同时配置,都会导致跨域在CorsInterceptorCorsFilter中各处理一次,降低程序运行效率,这种组合不可取。

    11.3Spring Security处理方案

    当为项目添加spring security依赖之后,通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,会统统失效;通过CorsFilter配置的跨域,有没有失效则要看过滤器的优先级,如果过滤器优先级高于spring security过滤器,即先执行,则配置的跨域处理仍然有效;如果低于spring security过滤器的优先级,则失效。
    为了理清楚这个问题,需要先简略了解一下FilterDispatcherServlet以及Interceptor执行顺序:

    在这里插入图片描述

    由于非简单请求都要首先发送一个预检请求,而预检请求并不会携带认证信息,所以预检请求就有被spring security拦截的可能。如果通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,最终都是在CorsInterceptor中对跨域请求进行校验的。要进入CorsInterceptor拦截器,肯定要先过spring security过滤器链,而在经过过滤器链时,由于预检请求没有携带认证信息,就会被拦截下来。
    如果使用了CorsFilter配置跨域,只要过滤器的优先级高于spring security过滤器,即在spring security过滤器之前就执行了跨域请求校验,那么就不会有问题。如果CorsFilter的优先级低于spring security过滤器,则预检请求一样需要先经过spring security的过滤器,由于没有携带认证信息,会被拦截下来。

    11.3.1特殊处理OPTIONS请求

    在引入spring security之后,如果还想继续通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,那么可以通过给OPTIONS请求单独放行,来解决预检请求被拦截的问题:

    // 不推荐使用,既不安全,也不优雅
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    // 指定所有的OPTIONS请求直接通过
                    .antMatchers(HttpMethod.OPTIONS).permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .httpBasic()
                    .and()
                    .csrf().disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    11.3.2继续使用CorsFilter

    只需要将CorsFilter的优先级设置高于spring security过滤器优先级:

    @Bean
    FilterRegistrationBean corsFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        registrationBean.setFilter(new CorsFilter(source));
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return registrationBean;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    过滤器的优先级,数字越小,优先级越高。
    当然也可以不设置最高优先级,只需要了解到spring security中FilterChainProxy过滤器的优先级,只要CorsFilter的优先级高于FilterChainProxy即可。
    Spring security中关于FilterChainProxy优先级的配置在SecurityFilterAutoConfiguration中。

    @Bean
    @ConditionalOnBean(name = DEFAULT_FILTER_NAME)
    public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) {
    	DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
    			DEFAULT_FILTER_NAME);
    	registration.setOrder(securityProperties.getFilter().getOrder());
    	registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
    	return registration;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到,过滤器的优先级是从SecurityProperties对象中读取的,该对象中默认的过滤器优先级是-100,即开发者配置的CorsFilter过滤器优先级只需要小于-100即可(开发者也可以在application.properties文件中,通过spring.security.filter.order配置去修改FilterChainProxy过滤器的默认优先级)。

    11.3.3专业解决方案

    Spring security中也提供了更加专业的方式来解决预检请求所面临的问题:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .httpBasic()
                    .and()
                    // 开启跨域配置
                    .cors()
                    .configurationSource(corsConfigurationSource())
                    .and()
                    .csrf().disable();
        }
    
        CorsConfigurationSource corsConfigurationSource() {
            // 提供CorsConfiguration实例,并配置跨域信息
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
            corsConfiguration.setAllowedMethods(Arrays.asList("*"));
            corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
            corsConfiguration.setMaxAge(3600L);
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", corsConfiguration);
            return source;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    cors()方法开启了对CorsConfigurer的配置,其最重要的方法就是configure方法:

    public void configure(H http) {
        ApplicationContext context = http.getSharedObject(ApplicationContext.class);
        // 获取一个CorsFilter并添加到spring security过滤器链中
        CorsFilter corsFilter = getCorsFilter(context);
        http.addFilter(corsFilter);
    }
    
    // 一共有4种不同的方式获取CorsFilter
    private CorsFilter getCorsFilter(ApplicationContext context) {
        // 1.如果configurationSource不为空,则直接根据它创建一个CorsFilter,前面的配置就是通过这种方式
        if (this.configurationSource != null) {
            return new CorsFilter(this.configurationSource);
        }
        // 2.判断spring容器中是否包含一个名为corsFilter的实例,如果包含,则取出并返回,意味着也可以直接向
        // 容器中注入一个corsFilter
        boolean containsCorsFilter = context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
        if (containsCorsFilter) {
            return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
        }
        // 3.判断spring容器中是否包含一个名为corsConfigurationSource的实例,如果包含,则据此创建CorsFilter并返回
        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);
        }
        // 4.HandlerMappingIntrospector是spring web中提供的一个类,该类实现了CorsConfigurationSource接口,
        // 所以也可以据此创建一个CorsFilter
        boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, context.getClassLoader());
        if (mvcPresent) {
            return MvcCorsFilter.getMvcCorsFilter(context);
        }
        return null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    拿到CorsFilter之后,调用http.addFilter方法将其添加到spring security过滤器链中,在过滤器链构建之前,会先对所有的过滤器进行排序,排序的依据在FilterOrderRegistration中已经定义好了:

    FilterOrderRegistration() {
        Step order = new Step(INITIAL_ORDER, ORDER_STEP);
        put(ChannelProcessingFilter.class, order.next());
        order.next(); // gh-8105
        put(WebAsyncManagerIntegrationFilter.class, order.next());
        put(SecurityContextPersistenceFilter.class, order.next());
        put(HeaderWriterFilter.class, order.next());
        put(CorsFilter.class, order.next());
        put(CsrfFilter.class, order.next());
        put(LogoutFilter.class, order.next());
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以看到,CorsFilter的位置在HeaderWriterFilter之后,在CsrfFilter之前,这个时候还没到认证过滤器。Spring security根据开发者提供的CorsConfigurationSource对象构建出一个CorsFilter,并将该过滤器置于认证过滤器之前。
    Spring security中关于跨域的这三种处理方式,在实际项目中推荐使用第三种。

  • 相关阅读:
    【信号去噪】基于麻雀算法优化VMD实现信号去噪附matlab代码
    路由、 网络、互联网、因特网、公网私网IP、NAT技术
    消息队列的一些场景及源码分析,RocketMQ使用相关问题及性能优化
    深度学习(五)softmax 回归之:分类算法介绍,如何加载 Fashion-MINIST 数据集
    Clickhouse初认识
    【毕业设计】12-基于单片机的电子体温计(原理图工程+源码工程+仿真工程+答辩论文)
    【数据治理】数据治理之主数据管理
    TypeScript简记(二)
    骑马钉 根据列行页数 生成 排序规则 java版 JavaScript版 python版
    深入理解强化学习——多臂赌博机:梯度赌博机算法的数学证明
  • 原文地址:https://blog.csdn.net/web18484626332/article/details/126052247