多个身份验证提供程序:如果身份验证失败,则不要委托

时间:2021-06-07 16:49:03

标签: spring spring-boot authentication spring-security

我正在尝试在 Spring 身份验证服务器 (Spring Security < 5.0) 中配置多个身份验证提供程序(主要和次要)。我知道将在主要提供商或第二个提供商上找到用户,永远不会同时出现。因此,如果主要提供商的身份验证失败,我想给出正确的消息。

根据“认证”方法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 会完成身份验证链并向客户端/用户报告,但似乎并非如此。

我错过了什么吗?

1 个答案:

答案 0 :(得分:0)

好的,刚刚想通了。

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 的一个较小版本。这个不会尝试捕获提供者发起的异常:

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

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