【问题标题】:Multiple Authentication Providers: do not delegate if authentication fails多个身份验证提供者:如果身份验证失败,请不要委托
【发布时间】:2021-08-24 18:40:32
【问题描述】:

我正在尝试在 Spring 身份验证服务器(Spring Security

根据“认证”方法documentation

返回:一个完全经过身份验证的对象,包括凭据。可能 如果 AuthenticationProvider 无法支持,则返回 null 传递的 Authentication 对象的身份验证。在这种情况下, 下一个 AuthenticationProvider 支持所呈现的 将尝试身份验证类。

抛出:AuthenticationException - 如果身份验证失败。

基于此,我在主提供者上实现了如下身份验证方法(我将省略 SecondaryAuthProvider 实现):

//PrimaryAuthProvider.class
public Authentication authenticate(Authentication authentication) {
    var user = authServices.getLdapUser(authentication.getName());

    //log and let the next provider handle it
    if (user == null) {
        logServices.userNotFound(new LogServices.AuthFailure(authentication.getName()));             
        return null;
    }

    if (passwordMatches(authentication.getCredentials(), user.getStringPassword())) {
        return authenticatedToken(user);
    } else {
        logServices.authFailure(new LogServices.AuthFailure(authentication.getName()));
        throw new BadCredentialsException("Invalid password");
    }
}

在 WebSecurity 中,我还注入了我的提供程序:

protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(primaryAuthProvider);
    auth.authenticationProvider(secondaryAuthProvider);
}

如果满足以下条件,这将正确处理:

  • 用户告知正确的登录名/密码,无论提供商如何。
  • 无论密码是否正确,都无法在主要提供商上找到用户。

如果在主要提供者上找到用户并且他的密码错误,则会抛出 BadCredentialsException,但服务器仍将委托给辅助提供者,最终消息将是“找不到用户”,这具有误导性。

我认为 BadCredentialsException 会完成身份验证链并报告给客户端/用户,但事实并非如此。

我错过了什么吗?

【问题讨论】:

    标签: spring spring-boot authentication spring-security


    【解决方案1】:

    好的,刚刚想通了。

    Provider Manager 是我服务器上使用的默认身份验证管理器。如果发生 AuthenticationException,它的身份验证方法确实会委托给下一个提供者:

    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }
    
        //(...)
    
        try {
            result = provider.authenticate(authentication);
    
            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }
        catch (AccountStatusException | InternalAuthenticationServiceException e) {
            prepareException(e, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw e;
        } catch (AuthenticationException e) {
            lastException = e;
        }
    }
    

    我找到了两种方法。

    第一个:如果身份验证失败,则在主提供者上提供未经身份验证的令牌并且不抛出任何异常:

    //PrimaryAuthProvider.class
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        var user = authServices.getLdapUser(authentication.getName());
    
        if (user == null) return null;
    
        if (passwordMatches(authentication.getCredentials(), user.getStringPassword())) {
            return authenticatedToken(user);
        } else {        
            return unauthenticatedToken(user);
        }
    }
    
    private UsernamePasswordAuthenticationToken unauthenticatedToken(LdapUser user)   {
            //Using 2 parameter constructor => authenticated = false 
            return new UsernamePasswordAuthenticationToken(
                user.getLogin(),
                user.getStringPassword(),
            );
    }
    

    这样做的缺点是会以英文显示默认消息。我需要在其他地方拦截异常并用葡萄牙语抛出一个新异常。

    第二个(希望我使用过):将我自己的 AuthorizationManager 实现为 ProviderManager 的较小版本。这个不会尝试捕获 Providers 发起的异常:

    public class CustomProviderManager implements AuthenticationManager {
        private final List<AuthenticationProvider> providers;
    
        public CustomProviderManager(AuthenticationProvider... providers) {
            this.providers = List.of(providers);
        }
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            for (var provider : providers) {
                if (!provider.supports(authentication.getClass())) continue;
    
                //let exceptions go through
                var result = provider.authenticate(authentication); 
                if (result != null) {
                    return result;
                }
            }
    
            throw new ProviderNotFoundException(
                "No provider for " + authentication.getName()
            );
        }
    }
    

    然后,在 WebSecurityConfig:

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return authenticationManager();
    }
    
    @Override
    protected AuthenticationManager authenticationManager() {
        return new CustomProviderManager(primaryAuthProvider, secondaryAuthProvider);
    }
    
    
    // Don't need it anymore
    //    @Override
    //    protected void configure(AuthenticationManagerBuilder auth) {
    //        auth.authenticationProvider(authenticationProvider);
    //        auth.authenticationProvider(secondaryAuthProvider);
    //    }
    

    第二个需要更多的编码,但给了我更多的控制权。

    【讨论】:

      猜你喜欢
      • 2015-12-22
      • 2020-11-09
      • 2020-07-26
      • 1970-01-01
      • 2017-11-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-28
      相关资源
      最近更新 更多