Spring Security 5.1.4 RELEASE
书接上文,在上一篇中介绍了Spring Security的基本组件以及基本的认证流程,此篇介绍一下Spring Security的一些核心服务。
Spring Security中还有许多十分重要的接口,特别是 AuthenticationManager
, UserDetailsService
和 AccessDecisionManager
,Spring Security提供了一些实现,用户亦可自己实现定制的认证授权机制。下面我们来具体看一下几个接口及Spring Security提供的实现,从而有助于了解在认证授权环节中它们的具体作用,以及如何使用。
AuthenticationManger
是一个接口,用来完成认证的逻辑,开发者可按照自己的项目设计需求进行实现。如果,我们希望能够组合使用多种认证服务,比如基于数据库和LDAP服务器的认证服务,Spring Security也是支持的。
ProviderManager
是Spring Security提供的一个实现,但是它本身不处理认证请求,而是将任务委托给一个配置好的 AuthenticationProvider
的列表,其中每一个 AuthenticationProvider
按序确认能否完成认证,每个provider如果认证失败,会抛出一个异常,如果认证通过,则会返回一个 Authentication
对象。
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
是 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
也是一个接口,用来完成具体的认证逻辑。不同的认证方式有不同的实现,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
是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; } }
可以看到 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,实现具体的认证逻辑 |
这些类和接口之间的关系,大致可以用下图进行表示