Spring Security管理下的ajax請求登錄超時問題處理

在系統中通常有這樣的情況,我們在發送ajax請求時,由於長時間沒操作系統,用戶登錄的session超時了,因此通過ajax得到的結果 ,直接就是登錄頁面的html代碼,而不是跳轉到登錄頁面或者是提示用戶,登錄已超時。

那麼,解決的方案就明顯了,就是要在spring返回登錄頁面之前 ,判斷該請求是否爲ajax請求,是則返回信息,提示用戶登錄已超時。

我們知道,在處理我們自定義的認證過濾器時,如果拋出一個AccessDeniedException, spring security就會幫我們跳轉到登錄頁上,那麼spring security是如何攔截並跳轉到?

這個我們得看下spring security的ExceptionTranslationFilter過濾器,它就是用於處理該過濾器之後的過濾器中所產生的異常並完成跳轉的。

這也就是,爲什麼我們自定義的認證過濾器,要配置在ExceptionTranslationFilter過濾器之後,才能被spring security攔截並響應。

接下來就先了解下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);
            }
        }
    }

<pre name="code" class="java">// 進行異常處理,捕捉到AuthenticationException和AccessDeniedException之後,都會去調用sendStartAuthentication方法進行處理
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) {
            if (authenticationTrustResolver.isAnonymous(SecurityContextHolder.getContext().getAuthentication())) {
                logger.debug("Access is denied (user is anonymous); 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);
            }
        }
    }

// 1、清除線程中的認證信息
// 2、將當前請求信息保存到cache中
// 3、進入認證入口,開始處理
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            AuthenticationException reason) throws ServletException, IOException {
        // SEC-112: Clear the SecurityContextHolder's Authentication, as the
        // existing Authentication is no longer considered valid
        SecurityContextHolder.getContext().setAuthentication(null);
        requestCache.saveRequest(request, response);
        logger.debug("Calling Authentication entry point.");
        authenticationEntryPoint.commence(request, response, reason);
    }


從上面的代碼我們可以看出,在spring security拋出認證異常之後,最終是交給authenticationEntryPoint這個入口切入點去處理的,處理登錄頁面跳轉的是LoginUrlAuthenticationEntryPoint這個類,以下是LoginUrlAuthenticationEntryPoint處理跳轉的代碼:


/**
     * Performs the redirect (or forward) to the login form URL.
     */
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {

        String redirectUrl = null;

        if (useForward) {

            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response, authException);

                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }

                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        } else {
            // redirect to login page. Use https if forceHttps true

            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

        }

        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }

既然我們知道了spring security在認證失敗時,是通過LoginUrlAuthenticationEntryPoint去處理頁面的跳轉的,那麼,我們就可以繼承處理登錄頁面跳轉的LoginUrlAuthenticationEntryPoint,然後加入我們對於ajax請求的處理,再替換掉spring security默認的登錄入口配置,文章開頭的問題就應該可以解決了。

public class MongoLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
	  @Override
	public void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException, ServletException {
		  HttpServletRequest httpRequest = (HttpServletRequest)request;
                // 處理 非匿名 的 ajax請求 或 帶有 isAjaxRequest=true 的請求,若session失效,則返回超時提示而不是拋出異常
                // 解決在session失效時,用ajax請求,返回的結果直接是登錄頁面
                if ("XMLHttpRequest".equals(httpRequest.getHeader("X-Requested-With"))
                     || "true".equalsIgnoreCase(httpRequest.getParameter("isAjaxRequest")){ 
	            response.setCharacterEncoding("UTF-8");
	            
	            ResultMessage resultMessage = new ResultMessage(ResultMessage.FAIL,"登錄超時,請重新登錄!");
	            response.getWriter().print(JSONObject.fromObject(resultMessage).toString()); 
	        } else{
	            super.commence(request, response, authException);
	        }
	}
}

然後將 security:http 的 entry-point-ref 改成我們上面擴展之後的MongoLoginUrlAuthenticationEntryPoint對應的bean定義

<security:http auto-config="true" entry-point-ref="mongoLoginUrlAuthenticationEntryPoint">
        <security:intercept-url pattern="/**" access="ROLE_LOGIN"/>
         <security:remember-me key="mongo_remenberPassword"/>
        <security:form-login login-page="/login.jsp"></security:form-login>
        <security:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" />
        <security:custom-filter ref="switchUserProcessingFilter" position="SWITCH_USER_FILTER" />
</security:http>

這樣,我們在處理ajax請求返回的結果時,依然可以按照之前的做法,檢測返回的json對象的狀態字段是否正常,然後提示給用戶就可以了

$('#mongoForm').ajaxSubmit(function(data){
					var obj=new com.MrMongo.form.ResultMessage(data);
					$.ligerDialog.closeWaitting();
					if(obj.isSuccess()){//成功
						$.ligerDialog.success("成功!");
				    }else{//失敗
				    	$.ligerDialog.err('出錯信息',"失敗",obj.getMessage());
				    	
				    }
	            });


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章