问题场景:

最近在研究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这个方法。

跟踪AuthenticationManagerBuilderperformBuild方法:
@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方法注入:

跟踪InitializeUserDetailsManagerConfigurerconfigure方法:
@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);发现这里会通过InitializeUserDetailsBeanManagerConfigurercontext获取spring容器中所有实现了security的UserDetailsService接口的Bean。如果并未自定义UserDetailsService实现的话,默认就只有InMemoryUserDetailsManager这一个,所以是不会进入beanNames.length != 1这个判断里面返回null的。
重点来了:当你自定义了一个UserDetailsService实现的时候beanNames的长度就会大于1了,这样就会导致进入beanNames.length != 1这个判断,并return null,导致InitializeUserDetailsManagerConfigurerconfigure无法注入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托管。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐