Spring Security 02 — Basic Introduction Part II

Spring Security 5.1.4 RELEASE

书接上文,在上一篇中介绍了Spring Security的基本组件以及基本的认证流程,此篇介绍一下Spring Security的一些核心服务。

图片[1]-Spring Security 02 — Basic Introduction Part II-Pikachu Hacker

2 核心服务

Spring Security中还有许多十分重要的接口,特别是AuthenticationManager, UserDetailsService 和AccessDecisionManager,Spring Security提供了一些实现,用户亦可自己实现定制的认证授权机制。下面我们来具体看一下几个接口及Spring Security提供的实现,从而有助于了解在认证授权环节中它们的具体作用,以及如何使用。

2.1 AuthenticationManager, ProviderManager and AuthenticationProvider

AuthenticationManger是一个接口,用来完成认证的逻辑,开发者可按照自己的项目设计需求进行实现。如果,我们希望能够组合使用多种认证服务,比如基于数据库和LDAP服务器的认证服务,Spring Security也是支持的。

ProviderManager是Spring Security提供的一个实现,但是它本身不处理认证请求,而是将任务委托给一个配置好的AuthenticationProvider的列表,其中每一个AuthenticationProvider按序确认能否完成认证,每个provider如果认证失败,会抛出一个异常,如果认证通过,则会返回一个Authentication对象。

 

AuthenticationManager

AuthenticationManger是一个接口,其中只有一个方法authenticate,用来尝试对传入的Authentication对象进行认证。

这里保留了源码中的一大段注释,其中包括了该接口的作用,以及在实现时应当注意的一些问题,这里不多做叙述,我们目前只需要了解这个接口的作用即可,感兴趣的同学可以自行阅读源码中的注释。

/** * Processes an {@link Authentication} request. * * @author Ben Alex */public interface AuthenticationManager {    // ~ Methods    // ========================================================================================================    /**     * Attempts to authenticate the passed {@link Authentication} object, returning a     * fully populated <code>Authentication</code> object (including granted authorities)     * if successful.     * <p>     * An <code>AuthenticationManager</code> must honour the following contract concerning     * exceptions:     * <ul>     * <li>A {@link DisabledException} must be thrown if an account is disabled and the     * <code>AuthenticationManager</code> can test for this state.</li>     * <li>A {@link LockedException} must be thrown if an account is locked and the     * <code>AuthenticationManager</code> can test for account locking.</li>     * <li>A {@link BadCredentialsException} must be thrown if incorrect credentials are     * presented. Whilst the above exceptions are optional, an     * <code>AuthenticationManager</code> must <B>always</B> test credentials.</li>     * </ul>     * Exceptions should be tested for and if applicable thrown in the order expressed     * above (i.e. if an account is disabled or locked, the authentication request is     * immediately rejected and the credentials testing process is not performed). This     * prevents credentials being tested against disabled or locked accounts.     *     * @param authentication the authentication request object     *     * @return a fully authenticated object including credentials     *     * @throws AuthenticationException if authentication fails     */    Authentication authenticate(Authentication authentication)            throws AuthenticationException;}

ProviderManager

ProviderManager是Authentication的一个实现,并将具体的认证操作委托给一系列的AuthenticationProvider来完成,从而可以实现支持多种认证方式。为了帮助阅读和理解源码具体做了什么,这里删除了原来的一部分注释,并对重要的部分进行了注释说明。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,        InitializingBean {    private static final Log logger = LogFactory.getLog(ProviderManager.class);    private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();    private List<AuthenticationProvider> providers = Collections.emptyList();    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();    private AuthenticationManager parent;    private boolean eraseCredentialsAfterAuthentication = true;    /**     * 用List<AuthenticationProvider>初始化一个ProviderManager     * 也就是上文提到的,ProviderManager将具体的认证委托给不同的provider,从而支持不同的认证方式     */          public ProviderManager(List<AuthenticationProvider> providers) {        this(providers, null);    }    /**     * 也可以为其设置一个父类     */    public ProviderManager(List<AuthenticationProvider> providers,            AuthenticationManager parent) {        Assert.notNull(providers, "providers list cannot be null");        this.providers = providers;        this.parent = parent;        checkState();    }    public void afterPropertiesSet() throws Exception {        checkState();    }    private void checkState() {        if (parent == null && providers.isEmpty()) {            throw new IllegalArgumentException(                    "A parent AuthenticationManager or a list "                            + "of AuthenticationProviders is required");        }    }    /**     * ProviderManager的核心方法,authentication方法尝试对传入的Authentication对象进行认证,传入的Authentication是     * 以用户的提交的认证信息,比如用户名和密码,创建的一个Authentication对象。     *      * 会依次询问各个AuthenticationProvider,当provider支持对传入的Authentication认证,     * 便会尝试使用该provider进行认证。如果有多个provider都支持认证传入的Authentication对象,     * 则只会使用第一个支持的provider进行认证。     *     * 一旦有一个provider认证成功了,便会忽略之前任何provider抛出的异常,之后的provider也不会再     * 继续认证的尝试。     *     * 如果所有provider都认证失败,方法则会抛出最后一个provider抛出的异常。     */    public Authentication authenticate(Authentication authentication)            throws AuthenticationException {        Class<? extends Authentication> toTest = authentication.getClass();        AuthenticationException lastException = null;        AuthenticationException parentException = null;        Authentication result = null;        Authentication parentResult = null;        boolean debug = logger.isDebugEnabled();        // 依次使用各个provider尝试进行认证        for (AuthenticationProvider provider : getProviders()) {            // 如果provider不支持对传入的Authentication进行认证,则跳过。            if (!provider.supports(toTest)) {                continue;            }            if (debug) {                logger.debug("Authentication attempt using "                        + provider.getClass().getName());            }            try {                // 调用provider的authenticate方法进行认证                result = provider.authenticate(authentication);                                // 如果认证成功,则将authentication中用户的细节信息复制到result中                // 然后跳出循环,不再尝试后面其他的provider                if (result != null) {                    copyDetails(authentication, result);                    break;                }            }            catch (AccountStatusException e) {                prepareException(e, authentication);                // 如果待认证的账号信息无误,但是账号本身异常,比如账号停用了,则抛出AccountStatusException异常,                // 并通过prepareException方法,发布一个AbstractAuthenticationFailureEvent,避免继续尝试其他provider进行认证                throw e;            }            catch (InternalAuthenticationServiceException e) {                prepareException(e, authentication);                throw e;            }            catch (AuthenticationException e) {                // 如果该provider认证失败,捕获异常AuthenticationException后不抛出,继续尝试下一个provider                // lastException会记录下最后一个认证失败的provider抛出的AuthenticationException异常。                lastException = e;            }        }        if (result == null && parent != null) {            // 如果所有provider都没能认证成功,则交给父类尝试认证            try {                result = parentResult = parent.authenticate(authentication);            }            catch (ProviderNotFoundException e) {                // 父类如果抛出该异常不做处理,因为后面有对子类抛出该异常的处理            }            catch (AuthenticationException e) {                // 父类也没能认证成功,则最后一个异常为来自父类认证失败的异常                lastException = parentException = e;            }        }        if (result != null) {            if (eraseCredentialsAfterAuthentication                    && (result instanceof CredentialsContainer)) {                // 认证成功,从Authentication中删除密码秘钥等敏感信息                ((CredentialsContainer) result).eraseCredentials();            }            // 如果父类认证成功,则会发布一个AuthenticationSuccessEvent,            // 这一步检查,防止子类重复发布            if (parentResult == null) {                eventPublisher.publishAuthenticationSuccess(result);            }                        //返回的result为一个Authentication,其中包含了已认证用户的信息            return result;                    }        if (lastException == null) {            lastException = new ProviderNotFoundException(messages.getMessage(                    "ProviderManager.providerNotFound",                    new Object[] { toTest.getName() },                    "No AuthenticationProvider found for {0}"));        }        // 如果父类认证失败,会发布一个AbstractAuthenticationFailureEvent        // 这一步检查,防止子类重复发布        if (parentException == null) {            prepareException(lastException, authentication);        }        throw lastException;    }    @SuppressWarnings("deprecation")    private void prepareException(AuthenticationException ex, Authentication auth) {        eventPublisher.publishAuthenticationFailure(ex, auth);    }    /**     * 从source中复制用户的信息到dest     *     * Copies the authentication details from a source Authentication object to a     * destination one, provided the latter does not already have one set.     *     * @param source source authentication     * @param dest the destination authentication object     */    private void copyDetails(Authentication source, Authentication dest) {        if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {            AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;            token.setDetails(source.getDetails());        }    }    public List<AuthenticationProvider> getProviders() {        return providers;    }    public void setMessageSource(MessageSource messageSource) {        this.messages = new MessageSourceAccessor(messageSource);    }    public void setAuthenticationEventPublisher(            AuthenticationEventPublisher eventPublisher) {        Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");        this.eventPublisher = eventPublisher;    }    /**     * 设置是否要在认证完成后,让Authentication调用自己的eraseCredentials方法来清除密码信息。     *     * If set to, a resulting {@code Authentication} which implements the     * {@code CredentialsContainer} interface will have its     * {@link CredentialsContainer#eraseCredentials() eraseCredentials} method called     * before it is returned from the {@code authenticate()} method.     *     * @param eraseSecretData set to {@literal false} to retain the credentials data in     * memory. Defaults to {@literal true}.     */    public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {        this.eraseCredentialsAfterAuthentication = eraseSecretData;    }    public boolean isEraseCredentialsAfterAuthentication() {        return eraseCredentialsAfterAuthentication;    }    private static final class NullEventPublisher implements AuthenticationEventPublisher {        public void publishAuthenticationFailure(AuthenticationException exception,                Authentication authentication) {        }        public void publishAuthenticationSuccess(Authentication authentication) {        }    }}

至此可以看到,ProviderManager的认证逻辑还是很简单清晰的,我们也可以比较清楚理解AuthenticationManager,ProviderManager和AuthenticationProvider的关系了。

 

AuthenticationProvider

AuthenticationProvider也是一个接口,用来完成具体的认证逻辑。不同的认证方式有不同的实现,Spring Security中提供了多种实现,包括DaoAuthenticationProvider,AnonymousAuthenticationProvider和LdapAuthenticationProvider等。其中最简单的DaoAuthenticationProvider会在后面介绍,首先来看一下AuthenticationProvider的源码。同样的,保留了源码中的注释,感兴趣的同学可以细读,这里只做简单的介绍。

可以看到,AuthenticationProvider中只有2个方法:

  • authenticate完成具体的认证逻辑,如果认证失败,抛出AuthenticationException异常

  • supports判断是否支持传入的Authentication认证信息

/** * Indicates a class can process a specific * {@link org.springframework.security.core.Authentication} implementation. * * @author Ben Alex */public interface AuthenticationProvider {    // ~ Methods    // ========================================================================================================    /**     * Performs authentication with the same contract as     * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}     * .     *     * @param authentication the authentication request object.     *     * @return a fully authenticated object including credentials. May return     * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support     * authentication of the passed <code>Authentication</code> object. In such a case,     * the next <code>AuthenticationProvider</code> that supports the presented     * <code>Authentication</code> class will be tried.     *     * @throws AuthenticationException if authentication fails.     */    Authentication authenticate(Authentication authentication)            throws AuthenticationException;    /**     * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the     * indicated <Code>Authentication</code> object.     * <p>     * Returning <code>true</code> does not guarantee an     * <code>AuthenticationProvider</code> will be able to authenticate the presented     * instance of the <code>Authentication</code> class. It simply indicates it can     * support closer evaluation of it. An <code>AuthenticationProvider</code> can still     * return <code>null</code> from the {@link #authenticate(Authentication)} method to     * indicate another <code>AuthenticationProvider</code> should be tried.     * </p>     * <p>     * Selection of an <code>AuthenticationProvider</code> capable of performing     * authentication is conducted at runtime the <code>ProviderManager</code>.     * </p>     *     * @param authentication     *     * @return <code>true</code> if the implementation can more closely evaluate the     * <code>Authentication</code> class presented     */    boolean supports(Class<?> authentication);}

DaoAuthenticationProvider

DaoAuthenticationProvider是Spring Security提供的最简单的一个AuthenticationProvider的实现,也是框架中最早支持的。它使用UserDetailsService作为一个DAO来查询用户名、密码以及用户的权限GrantedAuthority。它认证用户的方式就是简单的比较UsernamePasswordAuthenticationToken中由用户提交的密码和通过UserDetailsService查询获得的密码是否一致。

下面我们来看一下DaoAuthenticationProvider的源码,对于源码的说明也写在了注释中。

DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider,而后者实现了AuthenticationProvider接口。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {    /**     * The plaintext password used to perform     * PasswordEncoder#matches(CharSequence, String)}  on when the user is     * not found to avoid SEC-2056.     */    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";    private PasswordEncoder passwordEncoder;    /**     * The password used to perform     * {@link PasswordEncoder#matches(CharSequence, String)} on when the user is     * not found to avoid SEC-2056. This is necessary, because some     * {@link PasswordEncoder} implementations will short circuit if the password is not     * in a valid format.     */    private volatile String userNotFoundEncodedPassword;    private UserDetailsService userDetailsService;    private UserDetailsPasswordService userDetailsPasswordService;    public DaoAuthenticationProvider() {        setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());    }    @SuppressWarnings("deprecation")    protected void additionalAuthenticationChecks(UserDetails userDetails,            UsernamePasswordAuthenticationToken authentication)            throws AuthenticationException {                // 用户未提交密码,抛出异常BadCredentialsException        if (authentication.getCredentials() == null) {            logger.debug("Authentication failed: no credentials provided");            throw new BadCredentialsException(messages.getMessage(                    "AbstractUserDetailsAuthenticationProvider.badCredentials",                    "Bad credentials"));        }                // 从传入了Authentication对象中获取用户提交的密码        String presentedPassword = authentication.getCredentials().toString();                // 用passwordEncoder的matches方法,比较用户提交的密码和userDetails中查询到的正确密码。        // 由于用户密码的存放一般都是hash后保密的,因此userDetails获取到的密码一般是一个hash值,而用户提交        // 的是一个明文密码,因此需要对用户提交的密码进行同样的hash计算后再进行比较。        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 void doAfterPropertiesSet() throws Exception {        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");    }    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);        }    }    @Override    protected Authentication createSuccessAuthentication(Object principal,            Authentication authentication, UserDetails user) {        boolean upgradeEncoding = this.userDetailsPasswordService != null                && this.passwordEncoder.upgradeEncoding(user.getPassword());        if (upgradeEncoding) {            String presentedPassword = authentication.getCredentials().toString();            String newPassword = this.passwordEncoder.encode(presentedPassword);            user = this.userDetailsPasswordService.updatePassword(user, newPassword);        }        return super.createSuccessAuthentication(principal, authentication, user);    }    private void prepareTimingAttackProtection() {        if (this.userNotFoundEncodedPassword == null) {            this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);        }    }    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {        if (authentication.getCredentials() != null) {            String presentedPassword = authentication.getCredentials().toString();            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);        }    }    /**     * Sets the PasswordEncoder instance to be used to encode and validate passwords. If     * not set, the password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}     *     * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}     * types.     */    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");        this.passwordEncoder = passwordEncoder;        this.userNotFoundEncodedPassword = null;    }    protected PasswordEncoder getPasswordEncoder() {        return passwordEncoder;    }    public void setUserDetailsService(UserDetailsService userDetailsService) {        this.userDetailsService = userDetailsService;    }    protected UserDetailsService getUserDetailsService() {        return userDetailsService;    }    public void setUserDetailsPasswordService(            UserDetailsPasswordService userDetailsPasswordService) {        this.userDetailsPasswordService = userDetailsPasswordService;    }}

AbstractUserDetailsAuthenticationProvider

可以看到DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider, 而一个provider最核心的authenticate方法,便写在了AbstractUserDetailsAuthenticationProvider中,下面我们只关注一下authenticate这个方法的源码。

public Authentication authenticate(Authentication authentication)            throws AuthenticationException {        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,                () -> messages.getMessage(                        "AbstractUserDetailsAuthenticationProvider.onlySupports",                        "Only UsernamePasswordAuthenticationToken is supported"));        // 从传入的Authentication对象中获取用户名        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"                : authentication.getName();                // 根据用户名,从缓存中获取用户的UserDetails        boolean cacheWasUsed = true;        UserDetails user = this.userCache.getUserFromCache(username);                if (user == null) {            cacheWasUsed = false;            // 如果从缓存中没有获取到用户,则通过方法retrieveUser来获取用户信息            // retrieve方法为一个抽象方法,不同的子类中有不同的实现,而在子类中,一般又会通过UserDetailService来获取用户信息,返回UserDetails            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为具体的认证逻辑,是一个抽象方法,在子类中实现。            // 比如前文中DaoAuthenticationProvider中,便是比较用户提交的密码和UserDetails中的密码            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;            }        }        postAuthenticationChecks.check(user);        if (!cacheWasUsed) {            this.userCache.putUserInCache(user);        }        Object principalToReturn = user;        if (forcePrincipalAsString) {            principalToReturn = user.getUsername();        }        return createSuccessAuthentication(principalToReturn, authentication, user);    }

可以看到在DaoAuthenticationProvider中还用到UserDetailsService来查询用户的密码权限信息,并包装为UserDetails返回,然后与用户提交的用户名密码信息进行比较来完成认证。UserDetailsService和UserDetails在不同的provider中都会被用到,关于这两个接口的说明,在下一篇文章中介绍。

 

总结

最后我们总结一下这几个接口和类

名称 类型 说明
AuthenticationManager 接口 完成认证
ProviderManager 实现AuthenticationManager接口,将认证任务委托给不同的AuthenticationProvider
AuthenticationProvider 接口 具体认证逻辑的接口
AbstractUserDetailsAuthenticationProvider 抽象类 实现了AuthenticationProvider接口,获取用户信息并完成认证的抽象类
DaoAuthenticationProvider 继承AbstractUserDetailsAuthenticationProvider,实现具体的认证逻辑

这些类和接口之间的关系,大致可以用下图进行表示

图片[2]-Spring Security 02 — Basic Introduction Part II-Pikachu Hacker

本文作者:小米SRC

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/102579.html

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容