Spring Security是一個可以爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。css
本文是接上一章Spring Security認證過程進一步分析Spring Security
用戶名密碼登陸受權是如何實現得;java
使用debug方式啓動https://github.com/longfeizheng/logback該項目,瀏覽器輸入http://localhost:8080/persons,用戶名隨意,密碼123456便可;git
如圖所示,顯示了登陸認證過程當中的 filters
相關的調用流程,做者將幾個自認爲重要的 filters 標註了出來,github
從圖中能夠看出執行的順序。來看看幾個做者認爲比較重要的 Filter 的處理邏輯,UsernamePasswordAuthenticationFilter
,AnonymousAuthenticationFilter
,ExceptionTranslationFilter
,FilterSecurityInterceptor
以及相關的處理流程以下所述;express
整個調用流程是,先調用其父類 AbstractAuthenticationProcessingFilter.doFilter() 方法,而後再執行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法進行驗證;編程
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; #1.判斷當前的filter是否能夠處理當前請求,不能夠的話則交給下一個filter處理 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { #2.抽象方法由子類UsernamePasswordAuthenticationFilter實現 authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } #2.認證成功後,處理一些與session相關的方法 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); #3.認證失敗後的的一些操做 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } #3. 認證成功後的相關回調方法 主要將當前的認證放到SecurityContextHolder中 successfulAuthentication(request, response, chain, authResult); }
整個程序的執行流程以下:瀏覽器
attemptAuthentication
進行驗證,該方法由子類UsernamePasswordAuthenticationFilter
實現protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }
1. 將當前認證成功的 Authentication 放置到 SecurityContextHolder 中; 2. 將當前認證成功的 Authentication 放置到 SecurityContextHolder 中; 3. 調用其它可擴展的 handlers 繼續處理該認證成功之後的回調事件;(實現`AuthenticationSuccessHandler`接口便可)
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { #1.判斷請求的方法必須爲POST請求 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } #2.從request中獲取username和password String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); #3.構建UsernamePasswordAuthenticationToken(兩個參數的構造方法setAuthenticated(false)) UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); #4. 調用 AuthenticationManager 進行驗證(子類ProviderManager遍歷全部的AuthenticationProvider認證) return this.getAuthenticationManager().authenticate(authRequest); }
POST
Authenticaiton
的實現類UsernamePasswordAuthenticationToken
,(UsernamePasswordAuthenticationToken
調用兩個參數的構造方法setAuthenticated(false))AuthenticationManager
的 authenticate
方法進行驗證;可參考ProviderManager部分;從上圖中過濾器的執行順序圖中能夠看出AnonymousAuthenticationFilter
過濾器是在UsernamePasswordAuthenticationFilter
等過濾器以後,若是它前面的過濾器都沒有認證成功,Spring Security
則爲當前的SecurityContextHolder
中添加一個Authenticaiton
的匿名實現類AnonymousAuthenticationToken
;安全
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { #1.若是前面的過濾器都沒認證經過,則SecurityContextHolder中Authentication爲空 if (SecurityContextHolder.getContext().getAuthentication() == null) { #2.爲當前的SecurityContextHolder中添加一個匿名的AnonymousAuthenticationToken SecurityContextHolder.getContext().setAuthentication( createAuthentication((HttpServletRequest) req)); if (logger.isDebugEnabled()) { logger.debug("Populated SecurityContextHolder with anonymous token: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } } else { if (logger.isDebugEnabled()) { logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } } chain.doFilter(req, res); } #3.建立匿名的AnonymousAuthenticationToken protected Authentication createAuthentication(HttpServletRequest request) { AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, principal, authorities); auth.setDetails(authenticationDetailsSource.buildDetails(request)); return auth; } /** * Creates a filter with a principal named "anonymousUser" and the single authority * "ROLE_ANONYMOUS". * * @param key the key to identify tokens created by this filter */ ##.建立一個用戶名爲anonymousUser 受權爲ROLE_ANONYMOUS public AnonymousAuthenticationFilter(String key) { this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); }
SecurityContextHolder中Authentication
爲否爲空;SecurityContextHolder
中添加一個匿名的AnonymousAuthenticationToken
(用戶名爲 anonymousUser 的AnonymousAuthenticationToken
)ExceptionTranslationFilter
異常處理過濾器,該過濾器用來處理在系統認證受權過程當中拋出的異常(也就是下一個過濾器FilterSecurityInterceptor
),主要是 處理 AuthenticationException
和 AccessDeniedException
。session
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 #.判斷是否是AuthenticationException Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { #. 判斷是否是AccessDeniedException 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); } } }
此過濾器爲認證受權過濾器鏈中最後一個過濾器,該過濾器以後就是請求真正的/persons
服務app
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); } #1. before invocation重要 InterceptorStatusToken token = super.beforeInvocation(fi); try { #2. 能夠理解開始請求真正的 /persons 服務 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } #3. after Invocation super.afterInvocation(token, null); } }
三個部分中,最重要的是 #1,該過程當中會調用 AccessDecisionManager
來驗證當前已認證成功的用戶是否有權限訪問該資源;
protected InterceptorStatusToken beforeInvocation(Object object) { ... Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); ... Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { #1.重點 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException)); throw accessDeniedException; } ... }
authenticated
就是當前認證的Authentication
,那麼object
和attributes
又是什麼呢?
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object);
咱們發現object
爲當前請求的 url:/persons
, 那麼getAttributes
方法就是使用當前的訪問資源路徑去匹配
咱們本身定義的匹配規則。
protected void configure(HttpSecurity http) throws Exception { http.formLogin()//使用表單登陸,再也不使用默認httpBasic方式 .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//若是請求的URL須要認證則跳轉的URL .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//處理表單中自定義的登陸URL .and() .authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM, SecurityConstants.DEFAULT_REGISTER_URL, "/**/*.js", "/**/*.css", "/**/*.jpg", "/**/*.png", "/**/*.woff2") .permitAll()//以上的請求都不須要認證 .anyRequest()//剩下的請求 .authenticated()//都須要認證 .and() .csrf().disable()//關閉csrd攔截 ; }
0-7
返回 permitALL
即不須要認證 ,8
對應anyRequest
返回 authenticated
即當前請求須要認證;
能夠看到當前的authenticated
爲匿名AnonymousAuthentication
用戶名爲anonymousUser
Spring Security
默認使用AffirmativeBased
實現AccessDecisionManager
的 decide
方法來實現受權
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; #1.調用AccessDecisionVoter 進行vote(投票) for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { #1.1只要有voter投票爲ACCESS_GRANTED,則經過 直接返回 case AccessDecisionVoter.ACCESS_GRANTED://1 return; @#1.2只要有voter投票爲ACCESS_DENIED,則記錄一下 case AccessDecisionVoter.ACCESS_DENIED://-1 deny++; break; default: break; } } if (deny > 0) { #2.若是有兩個及以上AccessDecisionVoter(姑且稱之爲投票者吧)都投ACCESS_DENIED,則直接就不經過了 throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }
deny++
,最後判斷if(deny>0
拋出AccessDeniedException
(未受權)public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) { assert authentication != null; assert fi != null; assert attributes != null; WebExpressionConfigAttribute weca = findConfigAttribute(attributes); if (weca == null) { return ACCESS_ABSTAIN; } EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi); ctx = weca.postProcess(ctx, fi); return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED; }
到此位置authentication
當前用戶信息,fl
當前訪問的資源路徑及attributes
當前資源路徑的決策(便是否須要認證)。剩下就是判斷當前用戶的角色Authentication.authorites
是否權限訪問決策訪問當前資源fi
。