Spring Security 源碼解析-----擴展未登錄處理類


在訪問需要登錄才可以訪問的接口時,直接跳轉到登錄頁。那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);
    }
View Code

認證成功后的回調方法:

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);
    }
View Code

認證失敗后的回調方法:

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);
View Code

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);
    }

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 

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

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;
        }
    }

接下來對未登錄訪問處理擴展

自定義:ZyLoginUrlAuthenticationEntryPoint 替換默認的LoginUrlAuthenticationEntryPoint

在配置類配置:

 http.
     ///...
      .exceptionHandling()
        .authenticationEntryPoint(new ZyLoginUrlAuthenticationEntryPoint("/loginPage"))

參考:

https://www.cnblogs.com/xuwenjin/p/9552303.html

https://blog.csdn.net/andy_zhang2007/article/details/85209178


免責聲明!

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



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