最近在研究spring cloud alibaba微服务,也研究到了OAuth2.0第三方授权。在实现的过程中决定使用成熟的spring security框架作为来实现授权登录及第三方授权功能。但在整合的过程中遇到了一个奇怪的问题。即两个springboot应用,都在同一个maven父工程下,两个应用的依赖是一样的,依赖的版本也是一样的,并且两个应用的Security配置类(即extends了WebSecurityConfigurerAdapter的配置类)都是一样的。但一个能正常运行,另外一个却一定要加入以下代码才能正常启动。
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
否则,如果在Security配置类中尝试调用super.authenticationManager()会返回null。因为我刚好写了一个自定义拦截器,需要注入AuthenticationManager给到我自定义的拦截器,所以就遇到了因为super.authenticationManager()返回null导致拦截器抛出authenticationManager can not be null
的异常。虽然我能通过上面显式暴露authenticationManagerBean的方式解决问题,我还是想知道当中的原因。
当我出现以上问题的时候,我也是通过度娘发现有人说可以显式定义一个authenticationManagerBean来解决问题。我尝试过后也的确解决了问题。问题虽然解决了,但是我在各大搜索引擎都搜索过,大家都没说其中的原理或原因,没说为什么这样写就能解决问题。所以我决定直接debug源码来发掘当中的原因。
从源码中发现,Security最后调用的是AuthenticationManagerBuilder类的protected ProviderManager performBuild() throws Exception
这个方法。
AuthenticationManagerBuilder
的performBuild
方法:@Override
protected ProviderManager performBuild() throws Exception {
// 就是这个判断返回了null
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
ProviderManager providerManager = new ProviderManager(authenticationProviders,
parentAuthenticationManager);
if (eraseCredentials != null) {
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}
从if判断中的debug信息No authenticationProviders and no parentAuthenticationManager defined. Returning null.
可知authenticationProviders为空或parentAuthenticationManager未定义导致返回null。
继续跟踪源码发现默认情况下authenticationProviders是从InitializeUserDetailsBeanManagerConfigurer的内部类InitializeUserDetailsManagerConfigurer的configure方法注入:
InitializeUserDetailsManagerConfigurer
的configure
方法:@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
if (auth.isConfigured()) {
return;
}
// 这里是关键,这里是会通过spring容器获取UserDetailsService的Bean,如果为空会直接返回
UserDetailsService userDetailsService = getBeanOrNull(
UserDetailsService.class);
if (userDetailsService == null) {
return;
}
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if (passwordEncoder != null) {
provider.setPasswordEncoder(passwordEncoder);
}
if (passwordManager != null) {
provider.setUserDetailsPasswordService(passwordManager);
}
provider.afterPropertiesSet();
// 如果上面的if (userDetailsService == null) 判断返回了,就来不到这里,也就注入不了provider到authenticationProviders中
auth.authenticationProvider(provider);
}
getBeanOrNull
方法private <T> T getBeanOrNull(Class<T> type) {
// 这里就是通过InitializeUserDetailsBeanManagerConfigurer挂载的spring容器获取所有实现了
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
.getBeanNamesForType(type);
if (beanNames.length != 1) {
return null;
}
return InitializeUserDetailsBeanManagerConfigurer.this.context
.getBean(beanNames[0], type);
}
分析以上代码中的String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type);
发现这里会通过InitializeUserDetailsBeanManagerConfigurer
的context
获取spring容器中所有实现了security的UserDetailsService
接口的Bean。如果并未自定义UserDetailsService
实现的话,默认就只有InMemoryUserDetailsManager
这一个,所以是不会进入beanNames.length != 1
这个判断里面返回null的。
重点来了:当你自定义了一个UserDetailsService
实现的时候beanNames
的长度就会大于1了,这样就会导致进入beanNames.length != 1
这个判断,并return null,导致InitializeUserDetailsManagerConfigurer
的configure
无法注入provider到AuthenticationManagerBuilder的authenticationProviders中,最终导致authenticationProviders为空,Security发现没有任何provider,也就构建不了一个默认的AuthenticationManager。
通过以上分析,可以发现Security会尝试获取全部的实现了UserDetailsService
接口的Bean,当Security发现你有自定义UserDetailsService
的时候,就不会自动构建一个默认的AuthenticationManager。
在这个场景中,我实现了UserDetailsService接口,但不注入Security。同时也不暴露一个默认的authenticationManagerBean所以会导致拿不到authenticationManager的情况。
Security的原则很简单:
发现你实现了UserDetailsService接口就不会自动构建一个默认的AuthenticationManager。这时候开发者就必须自定义AuthenticationManagerBuilder或者主动暴露一个authenticationManagerBean,否则AuthenticationManager就会是null
方案一:在Security配置类(即extends了WebSecurityConfigurerAdapter的配置类)中主动暴露一个默认的authenticationManagerBean
:
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
方案二:在Security配置类(即extends了WebSecurityConfigurerAdapter的配置类)中注入自定义的UserDetailsService
,构建一个自定义的AuthenticationManagerBuilder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService((UserDetailsService) userService)// 设置UserDetailsService
.passwordEncoder(new BCryptPasswordEncoder());// 使用BCrypt进行密码的hash
}
方案三:如果没用到,就不要自定义UserDetailsService
实现并作为Bean交给Spring托管。