现象
最近在使用springsecurity,想要做一个授权服务器。在使用过程中,调试断点的时候发现HttpSecurity的初始化会执行两次。这个是不符合预期的。因为我们都知道如果我们引入了springsecurity的依赖后,由于自动配置,springsecurity会自动配置一条默认的过滤器链。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
以前我们分析过SpringBootWebSecurityConfiguration这个配置类,它会检查如果用户没有主动配置一条SecurityFilterChain,框架会提供一个默认的过滤器链。@ConditionalOnDefaultWebSecurity这个注解是条件注解,它就是来确保如果用户没有自定义过滤器链,那么这个配置会生效。
less
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
/**
* The default configuration for web security. It relies on Spring Security's
* content-negotiation strategy to determine what sort of authentication to use. If
* the user specifies their own {@link SecurityFilterChain} bean, this will back-off
* completely and the users should specify all the bits that they want to configure as
* part of the custom security configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
}
}
如果上面的配置生效,框架在启动的时候就会调用defaultSecurityFilterChain(HttpSecurity http) 方法,并且注入一个干净的HttpSecurity实例。正常情况下springboot的依赖注入是单例模式的,但是HttpSecurity这个类比较特殊。HttpSecurityConfiguration在配置HttpSecurity的时候,将他的作用域scope设置为prototype,如下图所示:

这就意味着每当创建SecurityFilterChain的时候,需要注入HttpSecurity的话,框架就会自动调用HttpSecurity httpSecurity() 方法返回一个全新的HttpSecurity,它不是全局唯一的。
如今通过调试发现HttpSecurity httpSecurity() 方法竟然调用了两次。那么到底还有什么地方需要依赖注入HttpSecurity呢?
WebSecurityConfiguration
我们知道FilterChainProxy是整个springsecurity的入口。那么FilterChainProxy 实例是如何被创建的?其实是由WebSecurityConfiguration配置的。下面的代码摘自 WebSecurityConfiguration。
WebSecurityConfiguration最后在Filter springSecurityFilterChain() 方法中通过this.httpSecurity.build() 构建了一个FilterChainProxy的。
Spring Security 支持添加多个SecurityFilterChain,每个SecurityFilterChain负责不同的请求(比如依据请求地址进行区分),这样可以为不同的请求设置不同的认证规则
具体来说,当请求到达FilterChainProxy时,其内部会根据当前请求匹配得到对应的SecurityFilterChain,然后将请求依次转发给到该SecurityFilterChain中的所有 Security Filters。
kotlin
@Autowired(required = false)
private HttpSecurity httpSecurity;
@Autowired(required = false)
void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
this.securityFilterChains = securityFilterChains;
}
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
// 首先检查securityFilterChains是否为空
boolean hasFilterChain = !this.securityFilterChains.isEmpty();
if (!hasFilterChain) {
// 提供一条默认的过滤器链进行兜底
this.webSecurity.addSecurityFilterChainBuilder(() -> {
this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
this.httpSecurity.formLogin(Customizer.withDefaults());
this.httpSecurity.httpBasic(Customizer.withDefaults());
return this.httpSecurity.build();
});
}
// 将用户或者框架配置的SecurityFilterChain包装成builder,方便后续统一调用
for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
}
for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
customizer.customize(this.webSecurity);
}
return this.webSecurity.build();
}
上面创建的过程中首先检查securityFilterChains集合是否为空。如果配置了SecurityFilterChain(无论是用户自定义还是框架默认配置的)都会通过Autowired的方式注入到securityFilterChains集合中
ini
private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();
因为FilterChainProxy依赖securityFilterChains来处理请求 如果过滤器链为空 那么构建FilterChainProxy就没有任何意义了。所以框架做了一次安全检查,如果为空,那么就执行下面的代码。
kotlin
this.webSecurity.addSecurityFilterChainBuilder(() -> {
this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
this.httpSecurity.formLogin(Customizer.withDefaults());
this.httpSecurity.httpBasic(Customizer.withDefaults());
return this.httpSecurity.build();
});
kotlin
public WebSecurity addSecurityFilterChainBuilder(
SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
this.securityFilterChainBuilders.add(securityFilterChainBuilder);
return this;
}
WebSecurity有个securityFilterChainBuilders集合,用来收集所有过滤器链的构建者。将在在执行build()的时候就可以取出这里面提前保存的构建者构建出一条过滤器链。
其实看到这里我们已经找到答案了。框架为我们进行了兜底操作,如果没有过滤器链,那么就通过lambda表达式包装成builder,后续构建出一条过滤器链使用,而构建的方法是this.httpSecurity.build()
所以 这就是第二次httpsecurity初始化的地方。在WebSecurityConfiguration中注入了httpsecurity实例,将来用来build出一条过滤器链。
下面的这段代码摘自WebSecurity,将来调用build方法的时候,最终会调用到performBuild方法中,这个方法我现在不细讲了,后面单独出一个文章专门来讲解WebSecurity构建filterProxy的过程。
kotlin
@Override
protected Filter performBuild() throws Exception {
Assert.state(!this.securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this is done by exposing a SecurityFilterChain bean. "
+ "More advanced users can invoke " + WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
for (RequestMatcher ignoredRequest : this.ignoredRequests) {
WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
+ ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}
if (this.privilegeEvaluator == null) {
this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
requestMatcherPrivilegeEvaluatorsEntries);
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (this.httpFirewall != null) {
filterChainProxy.setFirewall(this.httpFirewall);
}
if (this.requestRejectedHandler != null) {
filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
}
else if (!this.observationRegistry.isNoop()) {
CompositeRequestRejectedHandler requestRejectedHandler = new CompositeRequestRejectedHandler(
new ObservationMarkingRequestRejectedHandler(this.observationRegistry),
new HttpStatusRequestRejectedHandler());
filterChainProxy.setRequestRejectedHandler(requestRejectedHandler);
}
filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
if (this.debugEnabled) {
this.logger.warn("\n\n" + "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n");
result = new DebugFilter(filterChainProxy);
}
this.postBuildAction.run();
return result;
}
但是里面有个代码我想提一下 就是在构建过程中最终会遍历securityFilterChainBuilders所有的securityFilterChainBuilder,然后调用securityFilterChainBuilder的build()方法返回一条过滤器链。这和我上面提到的正好相呼应。通过包装成builder的方式,统一收集在securityFilterChainBuilders集合中,这里统一调用。
ini
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}
总结
终于找到HttpSecurity会被创建两次的关键原因了
1:构建SecurityFilterChain的时候需要注入HttpSecurity,框架一旦发现需要注入HttpSecurity,就会调用@bean标注的HttpSecurity httpSecurity()方法创建一个新实例 因为它是prototype类型的
2:WebSecurityConfiguration类为我们进行了兜底操作,防止我们不配置过滤器链。也注入了HttpSecurity实例,用于将来帮助开发者创建过滤器链
之所以执着的想要知道原因,是因为我讨厌那种黑盒子的感觉,不知道底层发生了什么,会感觉到恐慌。我将整个过程记录下来,一是如果大家恰好有这个疑虑那么正好这个文章会对您有帮助。而是我也是希望我自己真正明白了。