Spring Security异常覆盖

2020-06-13   


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);
	}
}

springsecurity1
而DaoAuthenticationConfigurer又会被添加到AbstractConfiguredSecurityBuilder中的configurers中。
在执行完
localConfigureAuthenticationBldr.build()->
AbstractConfiguredSecurityBuilder.doBuild()->
configure()->
AbstractDaoAuthenticationConfigurer.configure(builder)
这一系列调用后,最终向ProviderManager中添加一个DaoAuthenticationProvider对象。
springsecurity2

解决

最终的解决方案很简单,为了不过多改变原先的配置(毕竟Spring Security我可以说是一点不熟_(:з」∠)_),直接将原先抛出的UsernameNotFoundException换为任意一个不会在ProviderManager的authenticate方法中被捕获的异常即可。

public UserDetails login(){
    
    //todo
    //查询不到用户的情况下
    throw  new CustomException("用户【" + username + "】不存在!");

}
其他

简单记录了下Spring Security出现的异常信息覆盖问题,对于整体流程还是有些不到位比较模糊的地方,待以后深入学习。springsecurity1.png

Q.E.D.