Spring Security 實戰:Spring Boot 下的自動配置

  • 2019 年 12 月 10 日
  • 筆記

1. 前言

我們在前幾篇對 Spring Security 的用戶資訊管理機制,密碼機制進行了探討。沒有看的同學可通過 https://felord.cn 獲取。我們發現 Spring Security Starter相關的 Servlet 自動配置都在spring-boot-autoconfigure-2.1.9.RELEASE(當前 Spring Boot 版本為2.1.9.RELEASE) 模組的路徑org.springframework.boot.autoconfigure.security.servlet 之下。其實官方提供的Starter組件的自動配置你都能在spring-boot-autoconfigure-2.1.9.RELEASE下找到。今天我們進一步來解密 Spring Security 在 Spring Boot 的配置和使用。

2. Spring Boot 下 Spring Security 的自動配置

我們可以通過 org.springframework.boot.autoconfigure.security.servlet 路徑下找到 Spring Security 關於Servlet的自動配置類。我們來大致了解一下。

2.1 SecurityAutoConfiguration

  package org.springframework.boot.autoconfigure.security.servlet;      import org.springframework.boot.autoconfigure.EnableAutoConfiguration;    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;    import org.springframework.boot.autoconfigure.security.SecurityDataConfiguration;    import org.springframework.boot.autoconfigure.security.SecurityProperties;    import org.springframework.boot.context.properties.EnableConfigurationProperties;    import org.springframework.context.ApplicationEventPublisher;    import org.springframework.context.annotation.Bean;    import org.springframework.context.annotation.Configuration;    import org.springframework.context.annotation.Import;    import org.springframework.security.authentication.AuthenticationEventPublisher;    import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;      /**     * {@link EnableAutoConfiguration Auto-configuration} for Spring Security.     *     * @author Dave Syer     * @author Andy Wilkinson     * @author Madhura Bhave     * @since 1.0.0     */    @Configuration    @ConditionalOnClass(DefaultAuthenticationEventPublisher.class)    @EnableConfigurationProperties(SecurityProperties.class)    @Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,            SecurityDataConfiguration.class })    public class SecurityAutoConfiguration {          @Bean        @ConditionalOnMissingBean(AuthenticationEventPublisher.class)        public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {            return new DefaultAuthenticationEventPublisher(publisher);        }      }  

SecurityAutoConfiguration 顧名思義安全配置類。該類引入(@import)了 SpringBootWebSecurityConfigurationWebSecurityEnablerConfigurationSecurityDataConfiguration 三個配置類。 讓這三個模組的類生效。是一個複合配置,是 Spring Security 自動配置最重要的一個類之一。 Spring Boot 自動配置經常使用這種方式以達到靈活配置的目的,這也是我們研究 Spring Security 自動配置的一個重要入口 同時 SecurityAutoConfiguration 還將 DefaultAuthenticationEventPublisher 作為默認的 AuthenticationEventPublisher 注入 Spring IoC 容器。如果你熟悉 Spring 中的事件機制你就會知道該類是一個 Spring 事件發布器。該類內置了一個HashMap<String, Constructor<? extends AbstractAuthenticationEvent>>維護了認證異常處理和對應異常事件處理邏輯的映射關係,比如賬戶過期異常 AccountExpiredException 對應認證過期事件AuthenticationFailureExpiredEvent ,也就是說發生不同認證的異常使用不同處理策略。

2.2 SpringBootWebSecurityConfiguration

  @Configuration    @ConditionalOnClass(WebSecurityConfigurerAdapter.class)    @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)    @ConditionalOnWebApplication(type = Type.SERVLET)    public class SpringBootWebSecurityConfiguration {          @Configuration        @Order(SecurityProperties.BASIC_AUTH_ORDER)        static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {          }      }  

這個類是Spring Security 對 Spring Boot Servlet Web 應用的默認配置。核心在於WebSecurityConfigurerAdapter 適配器。從 @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) 我們就能看出 WebSecurityConfigurerAdapter 是安全配置的核心。 默認情況下 DefaultConfigurerAdapter 將以SecurityProperties.BASIC_AUTH_ORDER-5) 的順序注入 Spring IoC 容器,這是個空實現。如果我們需要個性化可以通過繼承 WebSecurityConfigurerAdapter 來實現。我們會在以後的博文重點介紹該類。

2.3 WebSecurityEnablerConfiguration

  @Configuration    @ConditionalOnBean(WebSecurityConfigurerAdapter.class)    @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)    @EnableWebSecurity    public class WebSecurityEnablerConfiguration {      }  

該配置類會在SpringBootWebSecurityConfiguration 注入 Spring IoC 容器後啟用 @EnableWebSecurity 註解。也就是說 WebSecurityEnablerConfiguration 目的僅僅就是在某些條件下激活 @EnableWebSecurity 註解。那麼這個註解都有什麼呢?

3. @EnableWebSecurity 註解

  @Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)    @Target(value = { java.lang.annotation.ElementType.TYPE })    @Documented    @Import({ WebSecurityConfiguration.class,            SpringWebMvcImportSelector.class,            OAuth2ImportSelector.class })    @EnableGlobalAuthentication    @Configuration    public @interface EnableWebSecurity {          /**         * Controls debugging support for Spring Security. Default is false.         * @return if true, enables debug support with Spring Security         */        boolean debug() default false;    }  

@Enable* 這類註解都是帶配置導入的註解。通過導入一些配置來啟用一些特定功能。 @EnableWebSecurity 導入了 WebSecurityConfigurationSpringWebMvcImportSelectorOAuth2ImportSelector 以及啟用了 @EnableGlobalAuthentication註解。

3.1 WebSecurityConfiguration

該配置類WebSecurityConfiguration使用一個WebSecurity對象基於用戶指定的或者默認的安全配置,你可以通過繼承 WebSecurityConfigurerAdapter 或者實現 WebSecurityConfigurer 來訂製 WebSecurity 創建一個FilterChainProxy Bean來對用戶請求進行安全過濾。這個FilterChainProxy的名稱就是 WebSecurityEnablerConfiguration上的 BeanIds.SPRING_SECURITY_FILTER_CHAIN 也就是 springSecurityFilterChain,它是一個Filter,最終會被作為Servlet過濾器鏈中的一個Filter應用到Servlet容器中。安全處理的策略主要是過濾器的調用順序。WebSecurityConfiguration 最終會通過 @EnableWebSecurity 應用到系統。

源碼分析:

  package org.springframework.security.config.annotation.web.configuration;      import java.util.Collections;    import java.util.List;    import java.util.Map;      import javax.servlet.Filter;      import org.springframework.beans.factory.BeanClassLoaderAware;    import org.springframework.beans.factory.annotation.Autowired;    import org.springframework.beans.factory.annotation.Value;    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;    import org.springframework.context.annotation.Bean;    import org.springframework.context.annotation.Configuration;    import org.springframework.context.annotation.DependsOn;    import org.springframework.context.annotation.ImportAware;    import org.springframework.core.OrderComparator;    import org.springframework.core.Ordered;    import org.springframework.core.annotation.AnnotationAttributes;    import org.springframework.core.annotation.AnnotationUtils;    import org.springframework.core.annotation.Order;    import org.springframework.core.type.AnnotationMetadata;    import org.springframework.security.access.expression.SecurityExpressionHandler;    import org.springframework.security.config.annotation.ObjectPostProcessor;    import org.springframework.security.config.annotation.SecurityConfigurer;    import org.springframework.security.config.annotation.web.WebSecurityConfigurer;    import org.springframework.security.config.annotation.web.builders.WebSecurity;    import org.springframework.security.context.DelegatingApplicationListener;    import org.springframework.security.web.FilterChainProxy;    import org.springframework.security.web.FilterInvocation;    import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;    import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;        /**     * Spring Web Security 的配置類 :     *  1. 使用一個 WebSecurity 對象基於安全配置創建一個 FilterChainProxy 對象來對用戶請求進行安全過濾。     *  2. 也會暴露諸如 安全SpEL表達式處理器 SecurityExpressionHandler 等一些類。     *     * @see EnableWebSecurity     * @see WebSecurity     *     * @author Rob Winch     * @author Keesun Baik     * @since 3.2     */    @Configuration    public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {        private WebSecurity webSecurity;     // 是否啟用了調試模式,來自註解 @EnableWebSecurity 的屬性 debug,預設值 false        private Boolean debugEnabled;          private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;          private ClassLoader beanClassLoader;          @Autowired(required = false)        private ObjectPostProcessor<Object> objectObjectPostProcessor;      /**       *       * 代理監聽器 應該時監聽 DefaultAuthenticationEventPublisher 的一些處理策略       */        @Bean        public static DelegatingApplicationListener delegatingApplicationListener() {            return new DelegatingApplicationListener();        }       /**        *        * 安全SpEL表達式處理器 SecurityExpressionHandler 預設為一個 DefaultWebSecurityExpressionHandler        */        @Bean        @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)        public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() {            return webSecurity.getExpressionHandler();        }          /**         *  Spring Security 核心過濾器  Spring Security Filter Chain  , Bean ID 為 springSecurityFilterChain         * @return the {@link Filter} that represents the security filter chain         * @throws Exception         */        @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)        public Filter springSecurityFilterChain() throws Exception {            boolean hasConfigurers = webSecurityConfigurers != null                    && !webSecurityConfigurers.isEmpty();            if (!hasConfigurers) {                WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor                        .postProcess(new WebSecurityConfigurerAdapter() {                        });                webSecurity.apply(adapter);            }            return webSecurity.build();        }          /**       *       * 用於模板 如JSP Freemarker 的一些頁面標籤按鈕控制支援         * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP         * tag support.         * @return the {@link WebInvocationPrivilegeEvaluator}         * @throws Exception         */        @Bean        @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)        public WebInvocationPrivilegeEvaluator privilegeEvaluator() throws Exception {            return webSecurity.getPrivilegeEvaluator();        }          /**       *       * 用於創建web configuration的SecurityConfigurer實例,       * 注意該參數通過@Value(...)方式注入,對應的bean autowiredWebSecurityConfigurersIgnoreParents       * 也在該類中定義       *         * @param objectPostProcessor the {@link ObjectPostProcessor} used to create a         * {@link WebSecurity} instance         * @param webSecurityConfigurers the         * {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to         * create the web configuration         * @throws Exception         */        @Autowired(required = false)        public void setFilterChainProxySecurityConfigurer(                ObjectPostProcessor<Object> objectPostProcessor,                @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)                throws Exception {            webSecurity = objectPostProcessor                    .postProcess(new WebSecurity(objectPostProcessor));            if (debugEnabled != null) {                webSecurity.debug(debugEnabled);            }              Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE);              Integer previousOrder = null;            Object previousConfig = null;            for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {                Integer order = AnnotationAwareOrderComparator.lookupOrder(config);                if (previousOrder != null && previousOrder.equals(order)) {                    throw new IllegalStateException(                            "@Order on WebSecurityConfigurers must be unique. Order of "                                    + order + " was already used on " + previousConfig + ", so it cannot be used on "                                    + config + " too.");                }                previousOrder = order;                previousConfig = config;            }            for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {                webSecurity.apply(webSecurityConfigurer);            }            this.webSecurityConfigurers = webSecurityConfigurers;        }       /**        * 從當前bean容器中獲取所有的WebSecurityConfigurer bean。        * 這些WebSecurityConfigurer通常是由開發人員實現的配置類,並且繼承自WebSecurityConfigurerAdapter        *        */        @Bean        public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(                ConfigurableListableBeanFactory beanFactory){            return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);        }          /**         * A custom verision of the Spring provided AnnotationAwareOrderComparator that uses         * {@link AnnotationUtils#findAnnotation(Class, Class)} to look on super class         * instances for the {@link Order} annotation.         *         * @author Rob Winch         * @since 3.2         */        private static class AnnotationAwareOrderComparator extends OrderComparator {            private static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator();              @Override            protected int getOrder(Object obj) {                return lookupOrder(obj);            }              private static int lookupOrder(Object obj) {                if (obj instanceof Ordered) {                    return ((Ordered) obj).getOrder();                }                if (obj != null) {                    Class<?> clazz = (obj instanceof Class ? (Class<?>) obj : obj.getClass());                    Order order = AnnotationUtils.findAnnotation(clazz, Order.class);                    if (order != null) {                        return order.value();                    }                }                return Ordered.LOWEST_PRECEDENCE;            }        }          /*         * 要是為了獲取註解 @EnableWebSecurity 的屬性 debugEnabled         *         * @see org.springframework.context.annotation.ImportAware#setImportMetadata(org.         * springframework.core.type.AnnotationMetadata)         */        public void setImportMetadata(AnnotationMetadata importMetadata) {            Map<String, Object> enableWebSecurityAttrMap = importMetadata                    .getAnnotationAttributes(EnableWebSecurity.class.getName());            AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes                    .fromMap(enableWebSecurityAttrMap);            debugEnabled = enableWebSecurityAttrs.getBoolean("debug");            if (webSecurity != null) {                webSecurity.debug(debugEnabled);            }        }          /*         * (non-Javadoc)         *         * @see         * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.         * lang.ClassLoader)         */        public void setBeanClassLoader(ClassLoader classLoader) {            this.beanClassLoader = classLoader;        }    }  

3.2 SpringWebMvcImportSelector

該類是為了對 Spring Mvc 進行支援的。一旦發現應用使用 Spring Mvc 的核心前置控制器 DispatcherServlet 就會引入 WebMvcSecurityConfiguration 。主要是為了適配 Spring Mvc 。

3.3 OAuth2ImportSelector

該類是為了對 OAuth2.0 開放授權協議進行支援。ClientRegistration 如果被引用,具體點也就是 spring-security-oauth2 模組被啟用(引入依賴jar)時。會啟用 OAuth2 客戶端配置 OAuth2ClientConfiguration

3.4 @EnableGlobalAuthentication

這個類主要引入了 AuthenticationConfiguration 目的主要為了構造 認證管理器 AuthenticationManagerAuthenticationManager 十分重要後面我們會進行專門的分析。多多關注公眾號:Felordcn 或者通過https://felord.cn 第一時間獲取相關講解。

4. SecurityFilterAutoConfiguration

我們在 org.springframework.boot.autoconfigure.security.servlet 路徑下還發現了一個配置類 SecurityFilterAutoConfiguration 。該類用於向Servlet容器註冊一個名稱為securityFilterChainRegistration的bean, 實現類是DelegatingFilterProxyRegistrationBean。該 bean 的目的是註冊另外一個 Servlet Filter BeanServlet 容器,實現類為 DelegatingFilterProxyDelegatingFilterProxy 其實是一個代理過濾器,它被 Servlet 容器用於處理請求時,會將任務委託給指定給自己另外一個Filter bean。對於 SecurityFilterAutoConfiguration,來講,這個被代理的Filter bean的名字為 springSecurityFilterChain , 也就是我們上面提到過的 Spring Security Web提供的用於請求安全處理的Filter bean,其實現類是 FilterChainProxy

相關的源碼分析:

  package org.springframework.boot.autoconfigure.security.servlet;      import java.util.EnumSet;    import java.util.stream.Collectors;      import javax.servlet.DispatcherType;      import org.springframework.boot.autoconfigure.AutoConfigureAfter;    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;    import org.springframework.boot.autoconfigure.security.SecurityProperties;    import org.springframework.boot.context.properties.EnableConfigurationProperties;    import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;    import org.springframework.context.annotation.Bean;    import org.springframework.context.annotation.Configuration;    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;    import org.springframework.security.config.http.SessionCreationPolicy;    import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;      @Configuration    // 僅在 Servlet 環境下生效    @ConditionalOnWebApplication(type = Type.SERVLET)    // 確保安全屬性配置資訊被載入並以bean形式被註冊到容器    @EnableConfigurationProperties(SecurityProperties.class)    // 僅在特定類存在於 classpath 上時才生效    @ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class,            SessionCreationPolicy.class })    // 指定該配置類在  SecurityAutoConfiguration 配置類應用之後應用    @AutoConfigureAfter(SecurityAutoConfiguration.class)    public class SecurityFilterAutoConfiguration {          // 要註冊到 Servlet 容器的 DelegatingFilterProxy Filter的        // 目標代理Filter bean的名稱 :springSecurityFilterChain        private static final String DEFAULT_FILTER_NAME =                AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;            // 定義一個 bean securityFilterChainRegistration,        // 該 bean 的目的是註冊另外一個 bean 到 Servlet 容器 : 實現類為 DelegatingFilterProxy 的一個 Servlet Filter        // 該 DelegatingFilterProxy Filter 其實是一個代理過濾器,它被 Servlet 容器用於匹配特定URL模式的請求,        // 而它會將任務委託給指定給自己的名字為 springSecurityFilterChain 的 Filter, 也就是 Spring Security Web        // 提供的用於請求安全處理的一個 Filter bean,其實現類是 FilterChainProxy        // (可以將 1 個 FilterChainProxy 理解為 1 HttpFirewall + n SecurityFilterChain)        @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;        }          private EnumSet<DispatcherType> getDispatcherTypes(                SecurityProperties securityProperties) {            if (securityProperties.getFilter().getDispatcherTypes() == null) {                return null;            }            return securityProperties.getFilter().getDispatcherTypes().stream()                    .map((type) -> DispatcherType.valueOf(type.name())).collect(Collectors                            .collectingAndThen(Collectors.toSet(), EnumSet::copyOf));        }      }  

5. 總結

本文主要對 Spring Security 在 Spring Boot 中的自動配置一些機制進行了粗略的講解。為什麼沒有細講。因為從學習出發有些東西不是我們必須要深入了解的,但是又要知道一點點相關的知識。我們先宏觀上有個大致的了解就行。所以在閱讀本文一定不要鑽牛角尖。粗略知道配置策略、載入策略和一些關鍵類的作用即可。在你對 Spring Security 有了進一步學習之後,回頭認真來看這些配置類會有更深層的思考。 從另一個方面該文也給你閱讀 Spring 源碼提供了一些思路,學會這些才是最重要的。