小米安全Dayeh:《Spring Security入坑指南2》
Spring Security 02 — Basic Introduction Part II
Spring Security 5.1.4 RELEASE
書接上文,在上一篇中介紹了Spring Security的基本元件以及基本的認證流程,此篇介紹一下Spring Security的一些核心服務。
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中都會被用到,關於這兩個介面的說明,在下一篇文章中介紹。
總結
最後我們總結一下這幾個介面和類
這些類和介面之間的關係,大致可以用下圖進行表示
相關文章
- 小米安全Dayeh:《Spring Security入坑指南1》2019-03-15Spring
- Spring Security安全綜合大全指南2024-04-15Spring
- Spring Security(二)登入與安全控制2018-09-22Spring
- Spring Security 安全框架2015-06-29Spring框架
- Spring Security OAuth2 單點登入2021-11-08SpringOAuth
- [譯] 學習 Spring Security(八):使用 Spring Security OAuth2 實現單點登入2018-04-08SpringOAuth
- Java安全框架(一)Spring Security2020-11-03Java框架Spring
- vim 入坑指南2018-03-20
- GreenDao入坑指南2017-12-14
- Spring Security Oauth22021-12-14SpringOAuth
- Spring Security 快速入門2019-03-02Spring
- Spring Security 入門篇2021-05-10Spring
- Spring Security(一)入門2018-09-15Spring
- Spring Security原始碼分析十二:Spring Security OAuth2基於JWT實現單點登入2018-01-26Spring原始碼OAuthJWT
- Spring Security入門(3-1)Spring Security的登入頁面定製2017-06-13Spring
- Spring Cloud Security:Oauth2實現單點登入2019-11-11SpringCloudOAuth
- Spring Security原始碼分析十一:Spring Security OAuth2整合JWT2019-02-28Spring原始碼OAuthJWT
- Spring Security原始碼分析十:初識Spring Security OAuth22018-01-22Spring原始碼OAuth
- UIStackView 入坑指南2019-01-04UIView
- Spring Security3.0入門2014-03-11Spring
- Spring Boot Security 整合 OAuth2 設計安全API介面服務2019-05-31Spring BootOAuthAPI
- Spring Security原始碼分析五:Spring Security實現簡訊登入2018-01-15Spring原始碼
- Spring security OAuth2 深入解析2017-12-22SpringOAuth
- 【Spring Security】1.快速入門2020-08-10Spring
- uni-app 入坑指南2019-02-22APP
- Oracle函式入坑指南2022-12-22Oracle函式
- rust入坑指南之ownership2023-02-22Rust
- React+Redux入坑指南2016-08-01ReactRedux
- Spring Boot 整合 Spring Security 入門案例教程2020-04-21Spring Boot
- Activiti7 與 Spring Boot 及 Spring Security 整合 踩坑記錄2021-07-14Spring Boot
- Spring Security 之 rememberMe 自動登入2020-06-26SpringREM
- Spring Security 入門原理及實戰2019-05-16Spring
- Spring Security使用(二) 非同步登入2020-11-29Spring非同步
- spring security 自定義認證登入2017-12-21Spring
- Spring入門指南2024-07-26Spring
- Omi 入坑指南 Third field 事件入門2018-11-29事件
- Go Web開發入坑指南2019-03-26GoWeb
- CTF萌新入坑指南(web篇)2020-09-27Web