在上一節我們討論了spring security過濾器的創建和注冊原理。請記住springSecurityFilterChain(類型為FilterChainProxy)是實際起作用的過濾器鏈,DelegatingFilterProxy起到代理作用。
但是這還沒有解決我們最初的所有問題,那就是雖然創建了springSecurityFilterChain過濾器鏈,那么過濾器鏈中的過濾器是如何一 一創建的?這些過濾器是如何實現認證和授權的?本節我們來討論這個問題。
注意:本節代碼示例,采用的依然是spring-security-4 (2)spring security 基於Java配置的搭建中的代碼為例。
一、過濾器的創建
我們創建的MySecurityConfig繼承了WebSecurityConfigurerAdapter。WebSecurityConfigurerAdapter中有個configure(HttpSecurity http)的方法:
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() //攔截請求,創建FilterSecurityInterceptor .anyRequest().authenticated() //在創建過濾器的基礎上的一些自定義配置 .and() //用and來表示配置過濾器結束,以便進行下一個過濾器的創建和配置 .formLogin().and() //設置表單登錄,創建UsernamePasswordAuthenticationFilter .httpBasic(); //basic驗證,創建BasicAuthenticationFilter }
該方法用來實現spring security的一些自定義的配置,其中就包括Filter的創建。其中http.authorizeRequests()、http.formLogin()、http.httpBasic()分別創建了ExpressionUrlAuthorizationConfigurer,FormLoginConfigurer,HttpBasicConfigurer。在三個類從父級一直往上找,會發現它們都是SecurityConfigurer的子類。SecurityConfigurer中又有configure方法。該方法被子類實現就用於創建各個過濾器,並將過濾器添加進HttpSecurity中維護的裝有Filter的List中,比如HttpBasicConfigurer中的configure方法,源碼如下:
public void configure(B http) throws Exception { AuthenticationManager authenticationManager = http .getSharedObject(AuthenticationManager.class); //創建BasicAuthenticationFilter過濾器 BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter( authenticationManager, this.authenticationEntryPoint); if (this.authenticationDetailsSource != null) { basicAuthenticationFilter .setAuthenticationDetailsSource(this.authenticationDetailsSource); } RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class); if(rememberMeServices != null) { basicAuthenticationFilter.setRememberMeServices(rememberMeServices); } basicAuthenticationFilter = postProcess(basicAuthenticationFilter); //添加過濾器 http.addFilter(basicAuthenticationFilter); }
另外,並非所有的過濾器都是在configure中進行創建的,比如UsernamePasswordAuthenticationFilter是在調用FormLoginConfigurer的構造方法時創建的。FormLoginConfigurer部分源碼如下:
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password"); }
HttpSecurity的父類是AbstractConfiguredSecurityBuilder,該類中有個configure方法用來獲取所有SecurityConfigurer,並調用所有SecurityConfigurer的configure方法。源碼如下:
private void configure() throws Exception { //獲取所有SecurityConfigurer類 Collection<SecurityConfigurer<O, B>> configurers = getConfigurers(); for (SecurityConfigurer<O, B> configurer : configurers) { //調用所有SecurityConfigurer的configure方法 configurer.configure((B) this); } }
以上就是過濾器的創建過程。當我們的MySecurityConfig繼承了WebSecurityConfigurerAdapter以后,就默認有了configure(HttpSecurity http)方法。我們也可以在MySecurityConfig中重寫此方法來進行更靈活的配置。
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() //注冊FilterSecurityInterceptor .antMatchers("/index.html").permitAll()//訪問index.html不要權限驗證 .anyRequest().authenticated()//其他所有路徑都需要權限校驗 .and() .csrf().disable()//默認開啟,可以顯示關閉 .formLogin() //內部注冊 UsernamePasswordAuthenticationFilter .loginPage("/login.html") //表單登錄頁面地址 .loginProcessingUrl("/login")//form表單POST請求url提交地址,默認為/login .passwordParameter("password")//form表單用戶名參數名 .usernameParameter("username") //form表單密碼參數名 .successForwardUrl("/success.html") //登錄成功跳轉地址 .failureForwardUrl("/error.html") //登錄失敗跳轉地址 //.defaultSuccessUrl()//如果用戶沒有訪問受保護的頁面,默認跳轉到頁面 //.failureUrl() //.failureHandler(AuthenticationFailureHandler) //.successHandler(AuthenticationSuccessHandler) //.failureUrl("/login?error") .permitAll();//允許所有用戶都有權限訪問loginPage,loginProcessingUrl,failureForwardUrl }
雖然我們上面僅僅看到了三種過濾器的創建,但是真正創建的遠不止三種,spring secuirty會默認幫我們注冊一些過濾器。比如SecurityContextPersistenceFilter,該過濾器用於在我們請求到來時,將SecurityContext從Session中取出放入SecuirtyContextHolder中供我們使用。並在請求結束時將SecuirtyContext存進Session中便於下次使用。還有DefaultLoginPageGeneratingFilter,該過濾器在我們沒有自定義配置loginPage時會自動生成,用於生成我們默認的登錄頁面,也就是我們一開始在搭建中看到的登錄頁面。對於自定義配置spring security詳細參考javaDoc。spring secuirty核心過濾器以及其順序如下(並未包括所有):
二、認證與授權
認證(Authentication):確定一個用戶的身份的過程。授權(Authorization):判斷一個用戶是否有訪問某個安全對象的權限。下面討論一下spring security中最基本的認證與授權。
首先明確一下在認證與授權中關鍵的三個過濾器,其他過濾器不討論:
1. UsernamePasswordAuthenticationFilter:該過濾器用於攔截我們表單提交的請求(默認為/login),進行用戶的認證過程吧。
2. ExceptionTranslationFilter:該過濾器主要用來捕獲處理spring security拋出的異常,異常主要來源於FilterSecurityInterceptor。
3. FilterSecurityInterceptor:該過濾器主要用來進行授權判斷。
下面根據我們訪問應用的順序並結合源碼分析一下spring security的認證與授權。代碼仍然是spring-security-4 (2)spring security 基於Java配置的搭建中的
1.我們在瀏覽器中輸入http://localhost:9090/ 訪問應用,因為我們的路徑被spring secuirty保護起來了,我們是沒有權限訪問的,所以我們會被引導至登錄頁面進行登錄。
此路徑因為不是表單提交的路徑(/login),該過程主要起作用的過濾器為FilterSecurityInterceptor。其部分源碼如下:
... public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } public void invoke(FilterInvocation fi) throws IOException, ServletException { //過濾器對每個請求只處理一次 if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //前處理 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { //使SecurityContextHolder中的Authentication保持原樣,因為RunAsManager會暫時改變 //其中的Authentication super.finallyInvocation(token); } //調用后的處理 super.afterInvocation(token, null); } } ...
真正進行權限判斷的為beforeInvocation,該方法定義在FilterSecurityInterceptor的父類AbstractSecurityInterceptor中,源碼如下:
... protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); //判斷object是否為過濾器支持的類型,在這里是FilterInvocation(里面記錄包含了請求的request,response,FilterChain) //這里可以把FilterInvocation看做是安全對象,因為通過它可以獲得request,通過request可以獲得請求的URI。 //而實際的安全對象就是URI if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( "Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + getSecureObjectClass()); } //獲取安全對象所對應的ConfigAttribute,ConfigAtrribute實際就是訪問安全所應該有的權限集。 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); //判斷安全對象是否擁有權限集,沒有的話說明所訪問的安全對象是一個公共對象,就是任何人都可以訪問的。 if (attributes == null || attributes.isEmpty()) { //如果rejectPublicInvocations為true,說明不支持公共對象的訪問,此時會拋出異常。 if (rejectPublicInvocations) { throw new IllegalArgumentException( "Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. " + "This indicates a configuration error because the " + "rejectPublicInvocations property is set to 'true'"); } if (debug) { logger.debug("Public object - authentication not attempted"); } publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } if (debug) { logger.debug("Secure object: " + object + "; Attributes: " + attributes); } //判斷SecurityCntext中是否存在Authentication,不存在則說明訪問着根本沒登錄 //調用下面的credentialsNotFound()方法則會拋出一個AuthenticationException, //該異常會被ExceptionTranslationFilter捕獲,並做出處理。 //不過默認情況下Authentication不會為null,因為AnonymouseFilter會默認注冊到 //過濾鏈中,如果用戶沒登錄的話,會將其當做匿名用戶(Anonymouse User)來對待。 //除非你自己將AnonymouseFilter從過濾鏈中去掉。 if (SecurityContextHolder.getContext().getAuthentication() == null) { credentialsNotFound(messages.getMessage( "AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } //Autentication存在,則說明用戶已經被認證(但是不表示已登錄,因為匿名用戶也是相當於被認證的), //判斷用戶是否需要再次被認證,如果你配置了每次訪問必須重新驗證,那么就會再次調用AuthenticationManager //的authenticate方法進行驗證。 Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //判斷用戶是否有訪問被保護對象的權限。 //ed。默認的AccessDesicisonManager的實現類是AffirmativeBased //AffirmativeBased采取投票的形式判斷用戶是否有訪問安全對象的權限 //票就是配置的Role。AffirmativeBased采用WebExpressionVoter進行投票 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } } ...
看這段代碼,請明確幾點。
a). beforeInvocation(Object object)中的object為安全對象,類型為FilterInvocation。安全對象就是受spring security保護的對象。雖然按道理來說安全對象應該是我們訪問的url,但是FilterInvocation中封裝了request,那么url也可以獲取到。
b). Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object) 每個安全對象都會有對應的訪問權限集(Collection<ConfigAttribute>),而且在容器啟動后所有安全對象的所有權限集就已經被獲取到並被放在安全元數據中(SecurityMetadataSource中),通過安全元數據可以獲取到各個安全對象的權限集。因為我們每個安全對象都是登錄才可以訪問的(anyRequest().authenticated()),這里我們只需要知道此時每個對象的權限集只有一個元素,並且是authenticated。如果一個對象沒有權限集,說明它是一個公共對象,不受spring security保護。
c). 當我們沒有登錄時,我們會被當做匿名用戶(Anonymouse)來看待。被當做匿名用戶對待是AnonymouseAuthenticationFilter來攔截封裝成一個Authentication對象,當用戶被認證后就會被封裝成一個Authentication對象。Authentication對象中封裝了用戶基本信息,該對象會在認證中做詳細介紹。AnonymouseAuthenticationFilter也是默認被注冊的。
d). 最中進行授權判斷的是AccessDecisionManager的子類AffirmativeBased的decide方法。我在來看其decide的源碼:
... public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; for (AccessDecisionVoter voter : getDecisionVoters()) { //根據用戶的authenticton和權限集得出能否訪問的結果 int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } if (deny > 0) { //如果deny>0說明沒有足夠的權限去訪問安全對象,此時拋出的 //AccessDeniedException會被ExceptionTranslationFilter捕獲處理。 throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); } ...
因為我們首次登錄,所以會拋出AccessDeniedexception。此異常會被ExceptionTranslationFilter捕獲並進行處理的。其部分源碼如下:
... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { chain.doFilter(request, response); logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); } if (ase != null) { //真正處理異常的地方 handleSpringSecurityException(request, response, chain, ase); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } // Wrap other Exceptions. This shouldn't actually happen // as we've already covered all the possibilities for doFilter throw new RuntimeException(ex); } } } private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { logger.debug( "Authentication exception occurred; redirecting to authentication entry point", exception); //未被認證,引導去登錄 sendStartAuthentication(request, response, chain, (AuthenticationException) exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { logger.debug( "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception); //如果為匿名用戶說明未登錄,引導去登錄 sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( "Full authentication is required to access this resource")); } else { logger.debug( "Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception); //用戶已登錄,但是沒有足夠權限去訪問安全對象,說明權限不足。進行 //權限不足的提醒 accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); } } } ...
因為我們是以匿名用戶的身份進行登錄的,所以,會被引導去登錄頁面。登錄頁面的創建是由默認注冊的過濾器DefaultLoginPageGeneratingFilter產生的。具體怎么產生的這里不做分析。我們只需要是誰做的就可以了。實際在使用時我們也不大可能去用默認生成的登錄頁面,因為太丑了。。。
2.在被引導至登錄頁面后,我們將輸入用戶名和密碼,提交至應用。應用會校驗用戶名和密碼,校驗成功后,我們成功訪問應用。
此時訪問的路徑為/login,這是UsernamePasswordAuthenticationFilter將攔截請求進行認證。UsernamePasswordAuthenticationFilter的doFilter方法定義在其父類AbstractAuthenticationProcessingFilter中,源碼如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //判斷請求是否需要進行驗證處理。默認對/login並且是POST請求的路徑進行攔截 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { //調用UsernamePasswordAuthenticationFilter的attemptAuthentication方法進行驗證,並返回 //完整的被填充的Authentication對象 authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } //進行session固定攻擊的處理 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // 認證失敗后的處理 unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //認證成功后的處理 successfulAuthentication(request, response, chain, authResult); }
實際認證發生在UsernamePasswordAuthenticationFilter的attemptAuthentication中,如果認證失敗,則會調用unsuccessfulAuthentication進行失敗后的處理,一般是提示用戶認證失敗,要求重新輸入用戶名和密碼,如果認證成功,那么會調用successfulAuthentication進行成功后的處理,一般是將Authentication存進SecurityContext中並跳轉至之前訪問的頁面或者默認頁面(這部分在讀者讀完本節后自行去看源碼是怎么處理的,這里不做討論,現在只需知道會跳到一開始我們訪問的頁面中)。下面我們來看認證即attemptAuthentication的源碼:
... public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); //將用戶名和密碼封裝在Authentication的實現UsernamePasswordAuthenticationToken //以便於AuthentictionManager進行認證 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); //獲得AuthenticationManager進行認證 return this.getAuthenticationManager().authenticate(authRequest); } ...
spring security在進行認證時,會將用戶名和密碼封裝成一個Authentication對象,在進行認證后,會將Authentication的權限等信息填充完全返回。Authentication會被存在SecurityContext中,供應用之后的授權等操作使用。此處介紹下Authentication,Authentication存儲的就是訪問應用的用戶的一些信息。下面是Authentication源碼:
public interface Authentication extends Principal, Serializable { //用戶的權限集合 Collection<? extends GrantedAuthority> getAuthorities(); //用戶登錄的憑證,一般指的就是密碼 Object getCredentials(); //用戶的一些額外的詳細信息,一般不用 Object getDetails(); //這里認為Principal就為登錄的用戶 Object getPrincipal(); //是否已經被認證了 boolean isAuthenticated(); //設置認證的狀態 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
講解了Authentication后,我們回過頭來再看attemptAuthentication方法,該方法會調用AuthenticationManager的authenticate方法進行認證並返回一個填充完整的Authentication對象。
在這里我們又要講解一下認證的幾個核心的類,很重要!
a). AuthenticationManager b).ProviderManager c).AuthenticationProvider d).UserDetailsService e).UserDetails
現在來說一下這幾個類的作用以及關聯關系。
a). AuthenticationManager是一個接口,提供了authenticate方法用於認證。
b). AuthenticationManager有一個默認的實現ProviderManager,其實現了authenticate方法。
c). ProviderManager內部維護了一個存有AuthenticationProvider的集合,ProviderManager實現的authenticate方法再調用這些AuthenticationProvider的authenticate方法去認證,表單提交默認用的AuthenticationProvider實現是DaoAuthenticationProvider。
d). AuthenticationProvider中維護了UserDetailsService,我們使用內存中的用戶,默認的實現是InMemoryUserDetailsManager。UserDetailsService用來查詢用戶的詳細信息,該詳細信息就是UserDetails。UserDetails的默認實現是User。查詢出來UserDetails后再對用戶輸入的密碼進行校驗。校驗成功則將UserDetails中的信息填充進Authentication中返回。校驗失敗則提醒用戶密碼錯誤。
以上說的這些接口的實現類是由我們在MySecurityConfig中配置時生成的,即下面的代碼
@Autowired public void configUser(AuthenticationManagerBuilder builder) throws Exception { builder .inMemoryAuthentication() //創建用戶名為user,密碼為password的用戶 .withUser("user").password("password").roles("USER"); }
這里不再討論具體是怎么生成的,記住即可。因為我們實際在項目中一般都會用自定義的這些核心認證類。
下面我們來分析源碼,先來看ProviderManager的authenticate方法:
... public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); //獲取所有AuthenticationProvider,循環進行認證 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { //對authentication進行認證 result = provider.authenticate(authentication); if (result != null) { //填充成完整的Authentication copyDetails(authentication, result); 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 = 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 = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } eventPublisher.publishAuthenticationSuccess(result); return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { //如果所有的AuthenticationProvider進行認證完result仍然為null //此時表示為提供AuthenticationProvider,拋出ProviderNotFoundException異常 lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; } ...
ProviderManager用AuthenticationProvider對authentication進行認證。如果沒有提供AuthenticationProvider,那么最終將拋出ProviderNotFoundException。
我們表單提交認證時,AuthenticationProvider默認的實現是DaoAuthenticationProvider,DaoAuthenticationProvider的authenticate方法定義在其父類AbstractUserDetailsAuthenticationProvider中,其源碼如下:
... 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 { //獲取UserDetails,即用戶詳細信息 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; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //認證成功,返回裝有用戶權限等信息的authentication對象 return createSuccessAuthentication(principalToReturn, authentication, user); } ...
retrieveUser方法定義在DaoAuthenticationProvider中,用來獲取UserDetails這里不再展示源碼,請讀者自行去看。你會發現獲取獲取UserDetails正是由其中維護的UserDetailsService來完成的。獲取到UserDetails后再調用其
additionalAuthenticationChecks方法進行密碼的驗證。如果認證失敗,則拋出AuthenticationException,如果認證成功則返回裝有權限等信息的Authentication對象。
三、總結
到目前為止,我們結合我們創建的項目和spring security的源碼分析了web應用認證和授權的原理。內容比較多,現在理一下重點。
1.springSecurityFilterChain中各個過濾器怎么創建的只需了解即可。不要太過關注。
2.重點記憶UsernamePasswordAuthenticationFilter,ExceptionTranslationFilter,FilterSecurityInterceptor這三個過濾器的作用及源碼分析。
3.重要記憶認證中Authentication,AuthenticationManager,ProviderManager,AuthenticationProvider,UserDetailsService,UserDetails這些類的作用及源碼分析。
4.重點記憶授權中FilterInvoction,SecurityMetadataSource,AccessDecisionManager的作用。
5.將這些類理解的關鍵是建立起關聯,建立起關聯的方式就是跟着本節中的案例走下去,一步步看代碼如何實現的。
參考資料:http://www.tianshouzhi.com/api/tutorials/spring_security_4/250
https://docs.spring.io/spring-security/site/docs/4.1.3.RELEASE/reference/htmlsingle/