AuthenticationProvider


UserDetailsService 在實作時,主要是依使用者名稱來取得 UserDetails,然而,在更複雜的驗證、授權情境下,不見得只會依使用者名稱來處理,如果你有自己的驗證實作方式,可以自行定義 AuthenticationProvider

實際上,運用 UserDetailsServiceDaoAuthenticationProvider,它繼承自 AbstractUserDetailsAuthenticationProvider,這個父類的實作介面之一就是 AuthenticationProvider,在 AbstractUserDetailsAuthenticationProvider 的實作中,authenticate 會呼叫抽象方法 retrieveUser 取得 UserDetails,然後呼叫抽象方法 additionalAuthenticationChecks 來進行驗證:

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            () -> messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.onlySupports",
                    "Only UsernamePasswordAuthenticationToken is supported"));

    // Determine username
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();

    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;

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

        Assert.notNull(user,
                "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,
                (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
        if (cacheWasUsed) {
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        else {
            throw exception;
        }
    }
    ...略
}

DaoAuthenticationProvider 在實作 retrieveUser 時,會透過 UserDetailsService 取得 UserDetails,而在 additionalAuthenticationChecks 運用 PasswordEncoder 比對密碼:

protected void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");

        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }

    String presentedPassword = authentication.getCredentials().toString();

    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");

        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }
}

... 略

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

這就是〈UserDetailsService〉中程式得以運作的原理了。

實際上,我們也可以實作 AuthenticationProvider,運用 gossip 既有的基礎來套用 Spring Security,在〈UserDetailsService〉的 gossip 專案基礎上,只需要修改 SecurityConfig 內容如下:

package cc.openhome.web;

... 略

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter  {
    @Autowired
    private AccountDAO accountDAO;

    @Autowired
    private UserService userService;

    ... 略

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new GossipAuthenticationProvider());
    }

    private class GossipAuthenticationProvider implements AuthenticationProvider {

        @Override
        public Authentication authenticate(Authentication authentication) 
          throws AuthenticationException {
            String name = authentication.getName();
            String password = authentication.getCredentials().toString();

            Optional<Account> acct = accountDAO.accountByUsername(name);

            if(acct.isPresent() && userService.login(name, password)) {
                return new UsernamePasswordAuthenticationToken(
                    name, 
                    password, 
                    AuthorityUtils.createAuthorityList("ROLE_MEMBER")
                );
            }

            return null;
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return authentication.equals(UsernamePasswordAuthenticationToken.class);
        }
    }
}

可以看到,AuthenticationManagerBuilder 可以呼叫 authenticationProvider 設定自定義的 AuthenticationProvider,由於是自定義的驗證提供者,當中不會運用 PasswordEncoder,也就不會需要〈UserDetailsService〉中處理密碼編碼的 workaround 作法了。

你可以在 gossip 中找到以上的範例專案。