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.EnableAutoConfiguration
,value=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 进行自定义配置,就要自定义这个类实例,通过覆盖类中方
法达到修改默认配置的目的: