01、Spring Security 实战 - 自动配置原理

1. 导入 SpringSecurity 依赖

<!--引入spring security依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. SpringBoot 的自动配置功能

使用Spring Boot时,我们只需引入对应的Starters,Spring Boot启动时便会自动加载相关依赖,配置相应的初始化参数,以最快捷、简单的形式对第三方软件进行集成,这便是Spring Boot的自动配置功能。

 

可以用一句话来描述整个过程:Spring Boot通过@EnableAutoConfiguration注解开启自动配置,加载spring.factories中注册的各种AutoConfiguration类,当某个AutoConfiguration类满足其注解@Conditional指定的生效条件(Starters提供的依赖、配置或Spring容器中是否存在某个Bean等)时,实例化该AutoConfiguration类中定义的Bean(组件等),并注入Spring容器,就可以完成依赖框架的自动配置。

  • @EnableAutoConfiguration:该注解由组合注解@SpringBootApplication引入,完成自动配置开启,扫描各个jar包下的spring.factories文件,并加载文件中注册的AutoConfiguration类等。
  • spring.factories:配置文件,位于jar包的META-INF目录下,按照指定格式注册了自动配置的AutoConfiguration类。spring.factories也可以包含其他类型待注册的类。该配置文件不仅存在于Spring Boot项目中,也可以存在于自定义的自动配置(或Starter)项目中。
  • AutoConfiguration类:自动配置类,代表了Spring Boot中一类以XXAutoConfiguration命名的自动配置类。其中定义了三方组件集成Spring所需初始化的Bean和条件。
  • @Conditional:条件注解及其衍生注解,在AutoConfiguration类上使用,当满足该条件注解时才会实例化AutoConfiguration类。
  • Starters:三方组件的依赖及配置,Spring Boot已经预置的组件。Spring Boot默认的Starters项目往往只包含了一个pom依赖的项目。如果是自定义的starter,该项目还需包含spring.factories文件、AutoConfiguration类和其他配置类。

1. 入口类和 @SpringBootApplication 注解

@EnableAutoConfiguration是开启自动配置的注解,在创建的Spring Boot项目中并不能直接看到此注解,它是由组合注解@SpringBootApplication引入的。

Spring Boot项目创建完成会默认生成一个*Application的入口类。通过该类的main方法即可启动Spring Boot项目。

@SpringBootApplication
public class SpringSecurity01Application {
   
     
    public static void main(String[] args) {
   
     
        SpringApplication.run(SpringSecurity01Application.class, args);
    }
}

在Spring Boot入口类(除单元测试外)中,唯一的一个注解就是@SpringBootApp-lication。它是Spring Boot项目的核心注解,用于开启自动配置,准确说是通过该注解内组合的@EnableAutoConfiguration开启了自动配置。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
   
      @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
   
     

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * 排除指定的自动配置类
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {
   
     };

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * 排除指定自动配置类名 
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {
   
     };

	/**
	 * Base packages to scan for annotated components. 
	 * 指定扫描的基础包,激活注解组件的初始化
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {
   
     };

	/**
	 * Type-safe alternative to {@linkscanBasePackages} for specifying the packages to
	 * scan for annotated components. The package of each class specified will be scanned.
	 * 指定扫描的类,用于初始化
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {
   
     };

	@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;
}

①@SpringBootConfiguration注解源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
   
     
	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;
}

②@EnableAutoConfiguration注解源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
   
     

   /**
    * Environment property that can be used to override when auto-configuration is
    * enabled.
    */
   String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

   /**
    * Exclude specific auto-configuration classes such that they will never be applied.
    */
   Class<?>[] exclude() default {
   
     };

   /**
    * Exclude specific auto-configuration class names such that they will never be
    * applied.
    */
   String[] excludeName() default {
   
     };

}

注解中的成员属性:

  • exclude:根据类(Class)排除指定的自动配置,该成员属性覆盖了@SpringBootApplication中组合的@EnableAutoConfiguration中定义的exclude成员属性。
  • excludeName:根据类名排除指定的自动配置,覆盖了@EnableAutoConfiguration中的excludeName的成员属性。
  • scanBasePackages:指定扫描的基础package,用于激活@Component等注解类的初始化。
  • Spring Boot中大量使用了@AliasFor注解,该注解用于桥接到其他注解,该注解的属性中指定了所桥接的注解类。如果点进去查看,会发现@SpringBootApplication定义的属性在其他注解中已经定义过了。之所以使用@AliasFor注解并重新在@SpringBootApplication中定义,更多是为了减少用户使用多注解带来的麻烦。

@SpringBootApplication注解中组合了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。因此,在实践过程中也可以使用这3个注解来替代@SpringBootApplication。

 

2. 注解 @EnableAutoConfiguration功能

在未使用Spring Boot的情况下,Bean的生命周期由Spring来管理,然而Spring无法自动配置@Configuration注解的类。而Spring Boot的核心功能之一就是根据约定自动管理该注解标注的类。用来实现该功能的组件之一便是@EnableAutoConfiguration注解。

@EnableAutoConfiguration位于spring-boot-autoconfigure包内,当使用@SpringBootApplication注解时,@EnableAutoConfiguration注解会自动生效。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
   
     

   /**
    * Environment property that can be used to override when auto-configuration is
    * enabled.
    * 用来覆盖配置开启/关闭自动配置的功能
    */
   String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

   /**
    * Exclude specific auto-configuration classes such that they will never be applied.
    * 根据类排除指定的自动配置
    */
   Class<?>[] exclude() default {
   
     };

   /**
    * Exclude specific auto-configuration class names such that they will never be
    * applied.
    * 根据类名排除指定的自动配置
    */
   String[] excludeName() default {
   
     };
}

@EnableAutoConfiguration注解提供了一个常量和两个成员参数的定义:

  • ENABLED_OVERRIDE_PROPERTY:用来覆盖开启/关闭自动配置的功能;
  • exclude:根据类(Class)排除指定的自动配置;
  • excludeName:根据类名排除指定的自动配置;

@EnableAutoConfiguration会猜测你需要使用的Bean,但如果在实战中你并不需要它预置初始化的Bean,可通过该注解的exclude或excludeName参数进行有针对性的排除。比如,当不需要数据库的自动配置时:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SpringSecurity01Application {
   
     
    public static void main(String[] args) {
   
     
        SpringApplication.run(SpringSecurity01Application.class, args);
    }
}

3. 条件注解 @Conditional

@Conditional注解是由Spring 4.0版本引入的新特性,可根据是否满足指定的条件来决定是否进行Bean的实例化及装配,比如,设定当类路径下包含某个jar包的时候才会对注解的类进行实例化操作。总之,就是根据一些特定条件来控制Bean实例化的行为,@Conditional注解代码如下:

@Target({
   
     ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
   
     

   Class<? extends Condition>[] value();
}

@Conditional注解唯一的元素属性是接口Condition的数组,只有在数组中指定的所有Condition的matches方法都返回true的情况下,被注解的类才会被加载。

@FunctionalInterface
public interface Condition {
   
     
   /**
    * Determine if the condition matches.
    */
   boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

matches方法的第一个参数为ConditionContext,可通过该接口提供的方法来获得Spring应用的上下文信息,ConditionContext接口定义如下:

public interface ConditionContext {
   
     

   /**
    * Return the {@link BeanDefinitionRegistry} that will hold the bean definition
    * should the condition match.
    * 
    */
   BeanDefinitionRegistry getRegistry();

   /**
    * Return the {@link ConfigurableListableBeanFactory} that will hold the bean
    * definition should the condition match, or {@code null} if the bean factory is
    * not available (or not downcastable to {@code ConfigurableListableBeanFactory}).
    */
   @Nullable
   ConfigurableListableBeanFactory getBeanFactory();

   /**
    * Return the {@link Environment} for which the current application is running.
    */
   Environment getEnvironment();

   /**
    * Return the {@link ResourceLoader} currently being used.
    */
   ResourceLoader getResourceLoader();

   /**
    * Return the {@link ClassLoader} that should be used to load additional classes
    * (only {@code null} if even the system ClassLoader isn't accessible).
    */
   @Nullable
   ClassLoader getClassLoader();
}

matches方法的第二个参数为AnnotatedTypeMetadata,该接口提供了访问特定类或方法的注解功能,并且不需要加载类,可以用来检查带有@Bean注解的方法上是否还有其他注解,AnnotatedTypeMetadata接口定义如下:

public interface AnnotatedTypeMetadata {
   
     

   MergedAnnotations getAnnotations();

   boolean isAnnotated(String annotationName);

   @Nullable
   Map<String, Object> getAnnotationAttributes(String annotationName);

   @Nullable
   Map<String, Object> getAnnotationAttributes(String annotationName,boolean classValuesAsString) ;
 
   @Nullable
   MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
 
   @Nullable
   MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
}

该接口的isAnnotated方法能够提供判断带有@Bean注解的方法上是否还有其他注解的功能。其他方法提供不同形式的获取@Bean注解的方法上其他注解的属性信息。

在Spring Boot的autoconfigure项目中提供了各类基于@Conditional注解的衍生注解,它们适用不同的场景并提供了不同的功能:

  • @ConditionalOnBean:在容器中有指定Bean的条件下;
  • @ConditionalOnClass:在classpath类路径下有指定类的条件下;
  • @ConditionalOnCloudPlatform:当指定的云平台处于active状态时;
  • @ConditionalOnExpression:基于SpEL表达式的条件判断;
  • @ConditionalOnJava:基于JVM版本作为判断条件;
  • @ConditionalOnMissingBean:当容器里没有指定Bean的条件时;
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件时;
  • @ConditionalOnProperty:在指定的属性有指定值的条件下;
  • @ConditionalOnResource:类路径是否有指定的值;
  • @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个或者有多个但是指定了首选的Bean时;
  • @ConditionalOnWebApplication:在项目是一个Web项目的条件下;

3. SpringSecurity的自动配置

1. SecurityAutoConfiguration

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SpringSecurity01Application {
   
     

    public static void main(String[] args) {
   
     
        SpringApplication.run(SpringSecurity01Application.class, args);
    }
}

Spring Boot通过@EnableAutoConfiguration注解开启自动配置,加载spring.factories中注册的各种AutoConfiguration类,当某个AutoConfiguration类满足其注解@Conditional指定的生效条件时,实例化该AutoConfiguration类中定义的Bean,并注入Spring容器,就可以完成依赖框架的自动配置。

首先,加载org.springframework.boot.autoconfigure包下META-INF/spring.factories中注册的key=org.springframework.boot.autoconfigure.EnableAutoConfigurationvalue=SecurityAutoConfiguration的自动配置类:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
...
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
...

当完成注册之后,在加载的过程中会使用元数据的配置进行过滤,对应的配置内容在META-INF/spring-autoconfigure-metadata.properties文件中:

org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.ConditionalOnClass=org.springframework.security.authentication.DefaultAuthenticationEventPublisher

在过滤的过程中要判断自动配置类SecurityAutoConfiguration是否被@ConditionalOnClass注解,源代码如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({
   
      SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
      SecurityDataConfiguration.class, ErrorPageSecurityFilterConfiguration.class })
public class SecurityAutoConfiguration {
   
     

   @Bean
   @ConditionalOnMissingBean(AuthenticationEventPublisher.class)
   public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
   
     
      return new DefaultAuthenticationEventPublisher(publisher);
   }
}

可以看到,该配置类SecurityAutoConfiguration被@ConditionalOnClass注解,并且指定实例化的条件为类路径下必须有DefaultAuthenticationEventPublisher类存在,如果Spring容器当中没有对应的类,则该类不会被注入。再看一下该类的其他注解:

  • @Configuration:指定该类作为配置项来进行实例化操作;
  • @Import:导入@Configuration注解类,这里导入了SpringBootWebSecurityConfiguration.class,WebSecurityEnablerConfiguration.class,SecurityDataConfiguration.class,ErrorPageSecurityFilterConfiguration.class。
  • @ConditionalOnMissingBean:注释于方法上,与@Bean配合,当Spring容器中没有该Bean的实例化对象时才会进行实例化。即当Spring容器中没有AuthenticationEventPublisher实例时,才会实例化DefaultAuthenticationEventPublisher对象,注入Spring容器。如果有了,那么就会覆盖掉当前默认的。
  • @EnableConfigurationProperties:参数为SecurityProperties.class,开启属性注入,可以通过在application.yaml 文件中配置这个 SecurityProperties 类中的属性。
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
     
       

   public static final int BASIC_AUTH_ORDER = Ordered.LOWEST_PRECEDENCE - 5;

   public static final int IGNORED_ORDER = Ordered.HIGHEST_PRECEDENCE;

   public static final int DEFAULT_FILTER_ORDER 
       = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;

   private final Filter filter = new Filter();
   private final User user = new User();

   public static class Filter {
     
       
       
      private int order = DEFAULT_FILTER_ORDER;
       
      private Set<DispatcherType> dispatcherTypes = new HashSet<>(
            Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
   }

   public static class User {
     
       
      /**
       * Default user name.
       */
      private String name = "user";

      /**
       * Password for the default user name.
       */
      private String password = UUID.randomUUID().toString();

      /**
       * Granted roles for the default user name.
       */
      private List<String> roles = new ArrayList<>();

      private boolean passwordGenerated = true;
   }
}

而在application.properties中,我们会进行如下对应配置,修改默认的用户名和密码:

spring.security.user.name=root
spring.security.user.password=root
spring.security.user.roles=admin,user

2. SpringBootWebSecurityConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
   
     

   @Bean
   @Order(SecurityProperties.BASIC_AUTH_ORDER)
   SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
   
     
      http.authorizeRequests()
           // 对所有的请求开启权限认证,认证之后才能访问
          .anyRequest().authenticated()
          .and()
           // 支持表单认证
          .formLogin()
          .and()
           // 支持basic认证
          .httpBasic();
      return http.build();
   }
}

@Configuration:说明该类SpringBootWebSecurityConfiguration会作为一个配置类的组件注入到Spring容器当中 ,并且不会生成对应的代理;

该类SpringBootWebSecurityConfiguration注入到Spring容器并实例化需要满足下面两个条件:

@ConditionalOnWebApplication(type = Type.SERVLET):参数为Type.SERVLET,说明该类只有在基于servlet的Web应用中才会被实例化。SpringBoot 集成了tomcat,默认就是一个servelet项目,因此条件满足。

@ConditionalOnDefaultWebSecurity:组合了注解@Conditional(DefaultWebSecurityCondition.class),该注解依赖于DefaultWebSecurityCondition类,

@Target({
   
      ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 条件注解,依赖于DefaultWebSecurityCondition类
@Conditional(DefaultWebSecurityCondition.class)
public @interface ConditionalOnDefaultWebSecurity {
   
     

}

如果@ConditionalOnDefaultWebSecurity 条件成立需要满足下面两个条件:

@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class }) :当classpath类路劲下存在SecurityFilterChain.class, HttpSecurity.classs 时,条件成立;

@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, WebSecurityConfigurerAdapter.class }):当Spring容器中不存在WebSecurityConfigurerAdapter,WebSecurityConfigurerAdapter 实例时,条件成立;

class DefaultWebSecurityCondition extends AllNestedConditions {
   
     

   DefaultWebSecurityCondition() {
   
     
      super(ConfigurationPhase.REGISTER_BEAN);
   }

   // 在classpath类路径下有指定类存在的条件下
   // 在classpath类路径下存在 SecurityFilterChain,HttpSecurity 类时,条件满足
   @ConditionalOnClass({
   
      SecurityFilterChain.class, HttpSecurity.class })
   static class Classes {
   
     

   }

   // 当容器里没有指定Bean的条件时
   // Spring的容器中不存在 WebSecurityConfigurerAdapter,SecurityFilterChain 实例时,条件满足
   // 如果Spring容器中存在WebSecurityConfigurerAdapter, SecurityFilterChain,说明我们对WebSecurityConfigurerAdapter, SecurityFilterChain进行了自定义,那么该条件不满足,Spring容器将不会实例化 DefaultWebSecurityCondition
   @ConditionalOnMissingBean({
   
      WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
   static class Beans {
   
     

   }
}

当Spring容器中含有 WebSecurityConfigurerAdapter 类及其子类时
,那么这个默认的 DefaultWebSecurityCondition 将不会进行配置了,所以这也就是为什么我们对SpringSecurity进行扩展的时候,需要继承 WebSecurityConfigurerAdapter 类来达成自定义配置了。

3. 总结

经过上面的分析可以看出,默认情况下,条件都是满足的。SpringSecurity 默认生效的是配置:

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
   
     

   @Bean
   @Order(SecurityProperties.BASIC_AUTH_ORDER)
   SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
   
     
      http.authorizeRequests()
           // 对所有的请求开启权限认证,认证之后才能访问
          .anyRequest().authenticated()
          .and()
           // 支持表单认证
          .formLogin()
          .and()
           // 支持basic认证
          .httpBasic();
      return http.build();
   }
}

WebSecurityConfigurerAdapter 这个类极其重要,Spring Security 核心配置都在这个类中,
如果想要扩展SpringSecurity的相关配置,可以在项目中自定义配置类继承WebSecurityConfigurerAdapter类或者实现SecurityFilterChain接口,这样操作都会覆盖掉上面的默认配置,SpringBoot 将所有的扩展配置都放在了WebSecurityConfigurerAdapter和SecurityFilterChain中。如果改配置可以扩展WebSecurityConfigurerAdapter,如果自定义过滤器可以扩展SecurityFilterChain。

如果要对 Spring Security 进行自定义配置,就要自定义这个类实例,通过覆盖类中方
法达到修改默认配置的目的: