Spring Security 源碼解析(一)


上篇 Spring Security基本配置已講述了Spring Security最簡單的配置,本篇將開始分析其基本原理

在上篇中可以看到,在訪問 http://localhost:18081/user 時,直接跳轉到登錄頁。那Security是怎么做的呢?本篇主要講述跳轉到登錄頁前的處理

首先來看一張時序圖:

通過上圖可以看到,請求順序為AbstractAuthenticationProcessingFilter -> AnonymousAuthenticationFilter -> ExceptionTranslationFilter -> FilterSecurityInterceptor

 

AbstractAuthenticationProcessingFilter

請求先進入 AbstractAuthenticationProcessingFilter 的doFilter()方法。判斷當前filter是否可以處理當前請求(也就是是否包含用戶名密碼信息),如果是,則調用其子類 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法進行驗證(第一次請求時,沒有用戶名密碼,是不會調用子類的)

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;      //判斷當前的filter是否可以處理當前請求,不可以的話則交給下一個filter處理 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { //抽象方法由子類UsernamePasswordAuthenticationFilter實現 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) { // Authentication failed  unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //認證成功后的相關回調方法,主要將當前的認證放到SecurityContextHolder中  successfulAuthentication(request, response, chain, authResult); }

認證成功后的回調方法:

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中  SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //調用其它可擴展的 handlers 繼續處理該認證成功以后的回調事件 //實現AuthenticationSuccessHandler接口即可  successHandler.onAuthenticationSuccess(request, response, authResult); }

認證失敗后的回調方法:

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { //清除SecurityContextHolder的中數據  SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString(), failed); logger.debug("Updated SecurityContextHolder to contain null Authentication"); logger.debug("Delegating to authentication failure handler " + failureHandler); } rememberMeServices.loginFail(request, response); //調用其它可擴展的 handlers 處理該認證失敗以后的回調事件 //實現 AuthenticationFailureHandler 接口即可  failureHandler.onAuthenticationFailure(request, response, failed); 

關於自定義 handlers ,可參考 Spring Security認證配置(三)

 

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter本身不是過濾器,而是繼承了AbstractAuthenticationProcessingFilter才擁有過濾器的性能,其主要是驗證用戶名密碼。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //認證請求的方法必須為POST if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //從request中獲取 username 和 password String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); //封裝Authenticaiton的實現類UsernamePasswordAuthenticationToken //傳入用戶名和密碼,並將是否已經認證設為false UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); //設置UsernamePasswordAuthenticationToken中的詳細信息。如remoteAddress、sessionId // Allow subclasses to set the "details" property  setDetails(request, authRequest); //調用 AuthenticationManager 的實現類 ProviderManager 進行驗證 return this.getAuthenticationManager().authenticate(authRequest); }

驗證的過程,可以參考AuthenticationManager、ProviderManager

 

ExceptionTranslationFilter

ExceptionTranslationFilter是異常處理過濾器,該過濾器用來處理在系統認證授權過程中拋出的異常(也就是FilterSecurityInterceptor拋出來的),主要是處理

AccessDeniedException、AuthenticationException

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) { //判斷是不是AuthenticationException // Try to extract a SpringSecurityException from the stacktrace 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); } } }

在 handleSpringSecurityException 方法中,有一段:

       Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            //判斷當前authentication是不是AnonymousAuthenticationToken(RememberMeAuthenticationToken)或者其子類 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")); }

其中sendStartAuthentication方法:

protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
     //清空SecurityContext SecurityContextHolder.getContext().setAuthentication(null); //緩存當前請求 requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); //調用AuthenticationEntryPoint的實現類LoginUrlAuthenticationEntryPoint(可擴展,實現AuthenticationEntryPoint即可) //跳轉到可配置的登錄頁(如果不配置,則跳轉到spring security默認的登錄頁) authenticationEntryPoint.commence(request, response, reason); }

 

FilterSecurityInterceptor 

此過濾器為認證授權過濾器鏈中最后一個過濾器,該過濾器通過之后就是真正的 /user 服務

public void invoke(FilterInvocation fi) throws IOException, ServletException {
        ......
else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //調用父類AbstractSecurityInterceptor.beforeInvocation方法,進行最后一次過濾 InterceptorStatusToken token = super.beforeInvocation(fi); try { //請求真正的 /user 服務 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }

在beforeInvocation方法中,會調用AccessDecisionManager.decide方法來驗證當前認證成功的用戶是否有權限訪問該資源

protected InterceptorStatusToken beforeInvocation(Object object) {
        ......

        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

        ......

        Authentication authenticated = authenticateIfRequired();

        // Attempt authorization
        try {
            //授權認證
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));

            throw accessDeniedException;
        }
    }

 上面的object和attributes是什么?調試一下:

 object為當前請求 URL:/user

 requestMap的值有兩個,如下圖:

可以看到,這兩個值對應SecurityConfig中的配置。第一個為antMatchs返回permitAll即不需要認證,第二個為anyRequest返回authenticated即其它請求需要認證

所以 getAttributes 就是使用當前請求路徑去匹配我們自定義的規則,attributes為匹配后的結果

我們繼續來看最核心的授權認證:

this.accessDecisionManager.decide(authenticated, object, attributes)

此時,authenticated為匿名AnonymousAuthenticationToken,attributes為authenticated

AccessDecisionManager是如何授權的呢?

Spring Security默認使用AccessDecisionManager的子類 AffirmativeBased,通過實現decide方法來鑒定用戶是否有訪問對應資源(方法或URL)的權限

public void decide(Authentication authentication, Object object,
            Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;

        //調用AccessDecisionVoter進行vote(投票)
        for (AccessDecisionVoter voter : getDecisionVoters()) {
            int result = voter.vote(authentication, object, configAttributes);

            if (logger.isDebugEnabled()) {
                logger.debug("Voter: " + voter + ", returned: " + result);
            }

            switch (result) {
            //voter投票為ACCESS_GRANTED,表示同意,直接返回
            case AccessDecisionVoter.ACCESS_GRANTED:
                return;
            //voter投票為ACCESS_DENIED,表示反對,則記錄一下
            case AccessDecisionVoter.ACCESS_DENIED:
                deny++;

                break;
            //voter投票為其它值,則表示棄權。都棄權也會通過
            default:
                break;
            }
        }

        //只要有一個voter投票為ACCESS_DENIED,則直接就不通過了
        if (deny > 0) {
            throw new AccessDeniedException(messages.getMessage(
                    "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        }

        // To get this far, every AccessDecisionVoter abstained
        checkAllowIfAllAbstainDecisions();
    }

第一次請求,這里將拋出AccessDeniedException。然后被ExceptionTranslationFilter捕獲,跳轉到授權登錄認證頁面

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM