Spring Security异常覆盖
场景
项目中的网关系统使用了Zuul+Spring Security框架。在用户登录发现用户不存在时,抛出了异常"UserDetailsService returned null, which is an interface contract violation",而在项目中查询不到用户的情况下抛出的自定义异常却被"吃了"。
public UserDetails login(){
//todo
//查询不到用户的情况下
throw new UsernameNotFoundException("用户【" + username + "】不存在!");
}
原因分析
跟着Bug断点来到了上述抛出的UsernameNotFoundException异常处,往下走异常来到了自定义MyAuthenticationProvider(实现了AuthenticationProvider接口)的retrieveUser方法。这个地方将我们的UsernameNotFoundException继续抛出。
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
try {
UserDetails user = login();
return user;
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
继续走来到了AbstractUserDetailsAuthenticationProvider的authenticate方法,仍然向上抛出。
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
继续走来到了ProviderManager的authenticate方法。一个for循环,并且最终抛出的异常变量名叫lastException,基本上可以确定我们的UsernameNotFoundException在这里被覆盖掉了。
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
//UsernameNotFoundException继承自AuthenticationException
catch (AuthenticationException e) {
lastException = e;
}
}
......
throw lastException;
跟着断点继续走,来到了DaoAuthenticationProvider的retrieveUser方法。是不是很眼熟?没错它也实现了AuthenticationProvider,正是在这里抛出的InternalAuthenticationServiceException成为了我们最终的"lastexception"。
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
问题已经找到了,但是项目里明明只向AuthenticationManagerBuilder添加了一个自定义的MyAuthenticationProvider,为什么会出现默认实现的DaoAuthenticationProvider对象呢?
代码中在添加完MyAuthenticationProvider后又设置了userDetailsService。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider();
......
auth
.authenticationProvider(myAuthenticationProvider)
.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
这一步中在apply方法里传入的是DaoAuthenticationConfigurer对象,它在实例化时会调用父类的构造方法,而父类中持有了DaoAuthenticationProvider对象。
public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return apply(new DaoAuthenticationConfigurer<>(
userDetailsService));
}
public class DaoAuthenticationConfigurer<B extends ProviderManagerBuilder<B>, U extends UserDetailsService>
extends
AbstractDaoAuthenticationConfigurer<B, DaoAuthenticationConfigurer<B, U>, U> {
/**
* Creates a new instance
* @param userDetailsService
*/
public DaoAuthenticationConfigurer(U userDetailsService) {
super(userDetailsService);
}
}
而DaoAuthenticationConfigurer又会被添加到AbstractConfiguredSecurityBuilder中的configurers中。
在执行完
localConfigureAuthenticationBldr.build()->
AbstractConfiguredSecurityBuilder.doBuild()->
configure()->
AbstractDaoAuthenticationConfigurer.configure(builder)
这一系列调用后,最终向ProviderManager中添加一个DaoAuthenticationProvider对象。
解决
最终的解决方案很简单,为了不过多改变原先的配置(毕竟Spring Security我可以说是一点不熟_(:з」∠)_),直接将原先抛出的UsernameNotFoundException换为任意一个不会在ProviderManager的authenticate方法中被捕获的异常即可。
public UserDetails login(){
//todo
//查询不到用户的情况下
throw new CustomException("用户【" + username + "】不存在!");
}
其他
简单记录了下Spring Security出现的异常信息覆盖问题,对于整体流程还是有些不到位比较模糊的地方,待以后深入学习。
Q.E.D.