Spring Seuciry相關的內容看了實在是太多了,但總以爲仍是理解地不夠鞏固,仍是須要靠知識輸出作鞏固。 java
相關版本:spring
java: jdk 8 spring-boot: 2.1.6.RELEASE
一個認證過程,其實就是過濾器鏈上的一個綠色矩形Filter所要執行的過程。 數據庫
基本的認證過程有三步驟:springboot
Authentication
,交由AuthenticationManager
進行認證;AuthenticationManager
的默認實現ProviderManager
會經過AuthenticationProvider
對Authentication
進行認證,其自己不作認證處理;Authentication
返回;不然拋出異常,以表示認證不經過。要理解這個過程,能夠從類UsernamePasswordAuthenticationFilter
,ProviderManager
,DaoAuthenticationProvider
和InMemoryUserDetailsManager
(UserDetailsService
實現類,由UserDetailsServiceAutoConfiguration
默認配置提供)進行了解。只要建立一個含有spring-boot-starter-security
的springboot項目,在適當地打上斷點接口看到這個流程。app
) ide
請求到前臺
以後,負責該請求的前臺
會將請求的內容封裝爲一個Authentication
對象交給認證管理部門
,認證管理部門
僅管理認證部門
,不作具體的認證操做,具體的操做由與該前臺
相關的認證部門
進行處理。固然,每一個認證部門
須要判斷Authentication
是否爲該部門負責,是則由該部門負責處理,不然交給下一個部門處理。認證部門
認證成功以後會建立一個認證經過的Authentication
返回。不然要麼拋出異常表示認證不經過,要麼交給下一個部門處理。spring-boot
若是須要新增認證類型,只要增長相應的前臺(Filter)
和與該前臺(Filter)
想對應的認證部門(AuthenticationProvider)
就便可,固然也能夠增長一個與已有前臺對應的認證部門
。認證部門
會經過前臺
生成的Authentication
來判斷該認證是否由該部門負責,於是也許提供一個二者相互認同的Authentication
. 工具
認證部門
須要人員資料時,則能夠從人員資料部門
獲取。不一樣的系統有不一樣的人員資料部門
,須要咱們提供該人員資料部門
,不然將拿到空白檔案。固然,人員資料部門
不必定是惟一的,認證部門
能夠有本身的專屬資料部門
。post
上圖還能夠有以下的畫法:this
這個畫法可能會和FilterChain更加符合。每個前臺其實就是FilterChain中的一個,客戶拿着請求逐個前臺請求認證,找到正確的前臺以後進行認證判斷。
這裏的前臺Filter
僅僅指實現認證的Filter,Spring Security Filter Chain中處理這些Filter還有其餘的Filter,好比CsrfFilter
。若是非要給角色給他們,那麼就當他們是保安人員
吧。
Spring Security爲咱們提供了3個已經實現的Filter。UsernamePasswordAuthenticationFilter
,BasicAuthenticationFilter
和 RememberMeAuthenticationFilter
。若是不作任何個性化的配置,UsernamePasswordAuthenticationFilter
和BasicAuthenticationFilter
會在默認的過濾器鏈中。這兩種認證方式也就是默認的認證方式。
UsernamePasswordAuthenticationFilter
僅僅會對/login
路徑生效,也就是說UsernamePasswordAuthenticationFilter
負責發佈認證,發佈認證的接口爲/login
。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { ... public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } ... }
UsernamePasswordAuthenticationFilter
爲抽象類AbstractAuthenticationProcessingFilter
的一個實現,而BasicAuthenticationFilter
爲抽象類BasicAuthenticationFilter
的一個實現。這四個類的源碼提供了不錯的前臺(Filter)
實現思路。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
提供了認證先後須要作的事情,其子類只須要提供實現完成認證的抽象方法attemptAuthentication(HttpServletRequest, HttpServletResponse)
便可。使用AbstractAuthenticationProcessingFilter
時,須要提供一個攔截路徑(使用AntPathMatcher
進行匹配)來攔截對應的特定的路徑。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter
做爲實際的前臺,會將客戶端提交的username和password封裝成一個UsernamePasswordAuthenticationToken
交給認證管理部門(AuthenticationManager)
進行認證。如此,她的任務就完成了。
BasicAuthenticationFilter
該前臺(Filter)
只會處理含有Authorization
的Header,且小寫化後的值以basic
開頭的請求,不然該前臺(Filter)
不負責處理。該Filter會從header中獲取Base64編碼以後的username和password,建立UsernamePasswordAuthenticationToken
提供給認證管理部門(AuthenticationMananager)
進行認證。
前臺
接到請求以後,會從請求中獲取所需的信息,建立自家認證部門(AuthenticationProvider)
所認識的認證資料(Authentication)
,認證部門(AuthenticationProvider)
則主要是經過認證資料(Authentication)
的類型判斷是否由該部門處理。
public interface Authentication extends Principal, Serializable { // 該principal具備的權限。AuthorityUtils工具類提供了一些方便的方法。 Collection<? extends GrantedAuthority> getAuthorities(); // 證實Principal的身份的證書,好比密碼。 Object getCredentials(); // authentication request的附加信息,好比ip。 Object getDetails(); // 當事人。在username+password模式中爲username,在有userDetails以後能夠爲userDetails。 Object getPrincipal(); // 是否已經經過認證。 boolean isAuthenticated(); // 設置經過認證。 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
在Authentication
被認證以後,會保存到一個thread-local的SecurityContext中。
// 設置 SecurityContextHolder.getContext().setAuthentication(anAuthentication); // 獲取 Authentication existingAuth = SecurityContextHolder.getContext() .getAuthentication();
在寫前臺Filter
的時候,能夠先檢查SecurityContextHolder.getContext()
中是否已經存在經過認證的Authentication
了,若是存在,則能夠直接跳過該Filter。已經經過驗證的Authentication
建議設置爲一個不可修改的實例。
目前從Authentication
的類圖中看到的實現類,均爲Authentication
的抽象子類AbstractAuthenticationToken
的實現類。實現類有好幾個,與前面的講到的Filter相關的有UsernamePasswordAuthenticationToken
和RememberMeAuthenticationToken
。
AbstractAuthenticationToken
爲CredentialsContainer
和Authentication
的子類。實現了一些簡單的方法,但主要的方法還須要實現。該類的getName()
方法的實現能夠看到經常使用的principal類爲UserDetails
、AuthenticationPrincipal
和Princial
。若是有須要將對象設置爲principal,能夠考慮繼承這三個類中的一個。
public String getName() { if (this.getPrincipal() instanceof UserDetails) { return ((UserDetails) this.getPrincipal()).getUsername(); } if (this.getPrincipal() instanceof AuthenticatedPrincipal) { return ((AuthenticatedPrincipal) this.getPrincipal()).getName(); } if (this.getPrincipal() instanceof Principal) { return ((Principal) this.getPrincipal()).getName(); } return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString(); }
AuthenticationManager
是一個接口,認證Authentication
,若是認證經過以後,返回的Authentication
應該帶上該principal所具備的GrantedAuthority
。
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
該接口的註釋中說明,必須按照以下的異常順序進行檢查和拋出:
DisabledException
:帳號不可用LockedException
:帳號被鎖BadCredentialsException
:證書不正確Spring Security提供一個默認的實現ProviderManager
。認證管理部門(ProviderManager)
僅執行管理職能,具體的認證職能由認證部門(AuthenticationProvider)
執行。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ... 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 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(); for (AuthenticationProvider provider : getProviders()) { // #1, 檢查是否由該認證部門進行認證`AuthenticationProvider` if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // #2, 認證部門進行認證 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); // #3,認證經過則再也不進行下一個認證部門的認證,不然拋出的異常被捕獲,執行下一個認證部門(AuthenticationProvider) break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException e) { lastException = parentException = e; } } // #4, 若是認證經過,執行認證經過以後的操做 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). // #5,若是認證不經過,必然有拋出異常,不然表示沒有配置相應的認證部門(AuthenticationProvider) if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it if (parentException == null) { prepareException(lastException, authentication); } throw lastException; } ... }
當使用到Spring Security OAuth2的時候,會看到另外一個實現OAuth2AuthenticationManager
。
認證部門(AuthenticationProvider)
負責實際的認證工做,與認證管理部門(ProvderManager)
協同工做。也許其餘的認證管理部門(AuthenticationManager)
並不須要認證部門(AuthenticationProvider)
的協做。
public interface AuthenticationProvider { // 進行認證 Authentication authenticate(Authentication authentication) throws AuthenticationException; // 是否由該AuthenticationProvider進行認證 boolean supports(Class<?> authentication); }
該接口有不少的實現類,其中包含了RememberMeAuthenticationProvider
(直接AuthenticationProvider)和DaoAuthenticationProvider
(經過AbastractUserDetailsAuthenticationProvider
簡介繼承)。這裏重點講講AbastractUserDetailsAuthenticationProvider
和DaoAuthenticationProvider
。
顧名思義,AbastractUserDetailsAuthenticationProvider
是對UserDetails
支持的Provider,其餘的Provider,如RememberMeAuthenticationProvider就不須要用到UserDetails
。該抽象類有兩個抽象方法須要實現類完成:
// 獲取 UserDetails protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
retrieveUser()
方法爲校驗提供UserDetails
。先看下UserDetails:
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); // 帳號是否過時 boolean isAccountNonExpired(); // 帳號是否被鎖 boolean isAccountNonLocked(); // 證書(password)是否過時 boolean isCredentialsNonExpired(); // 帳號是否可用 boolean isEnabled(); }
AbastractUserDetailsAuthenticationProvider#authentication(Authentication)
分爲三步驗證:
preAuthenticationChecks
的默認實現爲DefaultPreAuthenticationChecks
,負責完成校驗:
UserDetails#isAccountNonLocked()
UserDetails#isEnabled()
UserDetails#isAccountNonExpired()
postAuthenticationChecks
的默認實現爲DefaultPostAuthenticationChecks
,負責完成校驗:
UserDetails#user.isCredentialsNonExpired()
additionalAuthenticationChecks
須要由實現類完成。
校驗成功以後,AbstractUserDetailsAuthenticationProvider
會建立並返回一個經過認證的Authentication
。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // Ensure we return the original credentials the user supplied, // so subsequent attempts are successful even with encoded passwords. // Also ensure we return the original getDetails(), so that future // authentication events after cache expiry contain the details UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }
以下爲DaoAuthenticationProvider
對AbstractUserDetailsAuthenticationProvider
抽象方法的實現。
// 檢查密碼是否正確 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")); } } // 經過資料室(UserDetailsService)獲取UserDetails對象 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; } ... }
在以上的代碼中,須要提供UserDetailsService
和PasswordEncoder
實例。只要實例化這兩個類,並放入到Spring容器中便可。
UserDetailsService
接口提供認證過程所需的UserDetails
的類,如DaoAuthenticationProvider
須要一個UserDetailsService
實例。
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
Spring Security提供了兩個UserDetailsService
的實現:InMemoryUserDetailsManager
和JdbcUserDetailsManager
。InMemoryUserDetailsManager
爲默認配置,從UserDetailsServiceAutoConfiguration
的配置中能夠看出。固然也不容易理解,基於數據庫的實現須要增長數據庫的配置,不適合作默認實現。這兩個類均爲UserDetailsManager
的實現類,UserDetailsManager
定義了UserDetails
的CRUD操做。InMemoryUserDetailsManager
使用Map<String, MutableUserDetails>
作存儲。
public interface UserDetailsManager extends UserDetailsService { void createUser(UserDetails user); void updateUser(UserDetails user); void deleteUser(String username); void changePassword(String oldPassword, String newPassword); boolean userExists(String username); }
若是咱們須要增長一個UserDetailsService
,能夠考慮實現UserDetailsService
或者UserDetailsManager
。
到這裏,咱們已經知道Spring Security的流程了。從上面的內容能夠知道,如要增長一個新的認證方式,只要增長一個[前臺(Filter)
+ 認證部門(AuthenticationProvider)
+ 資料室(UserDetailsService)
]組合便可。事實上,資料室(UserDetailsService)
不是必須的,可根據認證部門(AuthenticationProvider)
須要實現。
我會在另外一篇文章中以手機號碼+驗證碼登陸爲例進行講解。