Spring Security4.1.3實現攔截登錄后向登錄頁面跳轉方式(redirect或forward)返回被攔截界面


一、看下內部原理

簡化后的認證過程分為7步:

  1. 用戶訪問網站,打開了一個鏈接(origin url)。

  2. 請求發送給服務器,服務器判斷用戶請求了受保護的資源。

  3. 由於用戶沒有登錄,服務器重定向到登錄頁面

  4. 填寫表單,點擊登錄

  5. 瀏覽器將用戶名密碼以表單形式發送給服務器

  6. 服務器驗證用戶名密碼。成功,進入到下一步。否則要求用戶重新認證(第三步)

  7. 服務器對用戶擁有的權限(角色)判定: 有權限,重定向到origin url; 權限不足,返回狀態碼403("forbidden").

從第3步,我們可以知道,用戶的請求被中斷了。

用戶登錄成功后(第7步),會被重定向到origin url,spring security通過使用緩存的request,使得被中斷的請求能夠繼續執行。

 

使用緩存

用戶登錄成功后,頁面重定向到origin url。瀏覽器發出的請求優先被攔截器RequestCacheAwareFilter攔截,RequestCacheAwareFilter通過其持有的RequestCache對象實現request的恢復。

[java]  view plain  copy
 
  1. public void doFilter(ServletRequest request, ServletResponse response,  
  2.             FilterChain chain) throws IOException, ServletException {  
  3.           
  4.         // request匹配,則取出,該操作同時會將緩存的request從session中刪除  
  5.         HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(  
  6.                 (HttpServletRequest) request, (HttpServletResponse) response);  
  7.           
  8.         // 優先使用緩存的request  
  9.         chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,  
  10.                 response);  
  11.     }  

 

何時緩存

首先,我們需要了解下RequestCache以及ExceptionTranslationFilter。

RequestCache

RequestCache接口聲明了緩存與恢復操作。默認實現類是HttpSessionRequestCache。HttpSessionRequestCache的實現比較簡單,這里只列出接口的聲明:

 

[java]  view plain  copy
 
  1. public interface RequestCache {  
  2.   
  3.     // 將request緩存到session中  
  4.     void saveRequest(HttpServletRequest request, HttpServletResponse response);  
  5.       
  6.     // 從session中取request  
  7.     SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);  
  8.       
  9.     // 獲得與當前request匹配的緩存,並將匹配的request從session中刪除  
  10.     HttpServletRequest getMatchingRequest(HttpServletRequest request,  
  11.             HttpServletResponse response);  
  12.       
  13.     // 刪除緩存的request  
  14.     void removeRequest(HttpServletRequest request, HttpServletResponse response);  
  15. }  

 

 

 

 

 

 

ExceptionTranslationFilter

ExceptionTranslationFilter 是Spring Security的核心filter之一,用來處理AuthenticationException和AccessDeniedException兩種異常。

在我們的例子中,AuthenticationException指的是未登錄狀態下訪問受保護資源,AccessDeniedException指的是登陸了但是由於權限不足(比如普通用戶訪問管理員界面)。

ExceptionTranslationFilter 持有兩個處理類,分別是AuthenticationEntryPoint和AccessDeniedHandler。

ExceptionTranslationFilter 對異常的處理是通過這兩個處理類實現的,處理規則很簡單:

[java]  view plain  copy
 
  1. 規則1. 如果異常是 AuthenticationException,使用 AuthenticationEntryPoint 處理  
  2. 規則2. 如果異常是 AccessDeniedException 且用戶是匿名用戶,使用 AuthenticationEntryPoint 處理  
  3. 規則3. 如果異常是 AccessDeniedException 且用戶不是匿名用戶,如果否則交給 AccessDeniedHandler 處理。  


對應以下代碼

[java]  view plain  copy
 
  1. private void handleSpringSecurityException(HttpServletRequest request,  
  2.             HttpServletResponse response, FilterChain chain, RuntimeException exception)  
  3.             throws IOException, ServletException {  
  4.         if (exception instanceof AuthenticationException) {  
  5.             logger.debug(  
  6.                     "Authentication exception occurred; redirecting to authentication entry point",  
  7.                     exception);  
  8.   
  9.             sendStartAuthentication(request, response, chain,  
  10.                     (AuthenticationException) exception);  
  11.         }  
  12.         else if (exception instanceof AccessDeniedException) {  
  13.             if (authenticationTrustResolver.isAnonymous(SecurityContextHolder  
  14.                     .getContext().getAuthentication())) {  
  15.                 logger.debug(  
  16.                         "Access is denied (user is anonymous); redirecting to authentication entry point",  
  17.                         exception);  
  18.   
  19.                 sendStartAuthentication(  
  20.                         request,  
  21.                         response,  
  22.                         chain,  
  23.                         new InsufficientAuthenticationException(  
  24.                                 "Full authentication is required to access this resource"));  
  25.             }  
  26.             else {  
  27.                 logger.debug(  
  28.                         "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",  
  29.                         exception);  
  30.   
  31.                 accessDeniedHandler.handle(request, response,  
  32.                         (AccessDeniedException) exception);  
  33.             }  
  34.         }  
  35.     }  

 

AccessDeniedHandler 默認實現是 AccessDeniedHandlerImpl。該類對異常的處理是返回403錯誤碼。

[java]  view plain  copy
 
  1. public void handle(HttpServletRequest request, HttpServletResponse response,  
  2.             AccessDeniedException accessDeniedException) throws IOException,  
  3.             ServletException {  
  4.     if (!response.isCommitted()) {  
  5.         if (errorPage != null) {  // 定義了errorPage  
  6.             // errorPage中可以操作該異常  
  7.             request.setAttribute(WebAttributes.ACCESS_DENIED_403,  
  8.                     accessDeniedException);  
  9.   
  10.             // 設置403狀態碼  
  11.             response.setStatus(HttpServletResponse.SC_FORBIDDEN);  
  12.   
  13.             // 轉發到errorPage  
  14.             RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);  
  15.             dispatcher.forward(request, response);  
  16.         }  
  17.         else { // 沒有定義errorPage,則返回403狀態碼(Forbidden),以及錯誤信息  
  18.             response.sendError(HttpServletResponse.SC_FORBIDDEN,  
  19.                     accessDeniedException.getMessage());  
  20.         }  
  21.     }  
  22. }  

 

AuthenticationEntryPoint 默認實現是 LoginUrlAuthenticationEntryPoint, 該類的處理是轉發或重定向到登錄頁面

 

[java]  view plain  copy
 
  1. public void commence(HttpServletRequest request, HttpServletResponse response,  
  2.             AuthenticationException authException) throws IOException, ServletException {  
  3.   
  4.     String redirectUrl = null;  
  5.   
  6.     if (useForward) {  
  7.   
  8.         if (forceHttps && "http".equals(request.getScheme())) {  
  9.             // First redirect the current request to HTTPS.  
  10.             // When that request is received, the forward to the login page will be  
  11.             // used.  
  12.             redirectUrl = buildHttpsRedirectUrlForRequest(request);  
  13.         }  
  14.   
  15.         if (redirectUrl == null) {  
  16.             String loginForm = determineUrlToUseForThisRequest(request, response,  
  17.                     authException);  
  18.   
  19.             if (logger.isDebugEnabled()) {  
  20.                 logger.debug("Server side forward to: " + loginForm);  
  21.             }  
  22.   
  23.             RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);  
  24.       
  25.             // 轉發  
  26.             dispatcher.forward(request, response);  
  27.   
  28.             return;  
  29.         }  
  30.     }  
  31.     else {  
  32.         // redirect to login page. Use https if forceHttps true  
  33.   
  34.         redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);  
  35.   
  36.     }  
  37.       
  38.     // 重定向  
  39.     redirectStrategy.sendRedirect(request, response, redirectUrl);  
  40. }  

 

 

 

 

 

 

了解完這些,回到我們的例子。

第3步時,用戶未登錄的情況下訪問受保護資源,ExceptionTranslationFilter會捕獲到AuthenticationException異常(規則1)。頁面需要跳轉,ExceptionTranslationFilter在跳轉前使用requestCache緩存request。

[java]  view plain  copy
 
  1. protected void sendStartAuthentication(HttpServletRequest request,  
  2.             HttpServletResponse response, FilterChain chain,  
  3.             AuthenticationException reason) throws ServletException, IOException {  
  4.     // SEC-112: Clear the SecurityContextHolder's Authentication, as the  
  5.     // existing Authentication is no longer considered valid  
  6.     SecurityContextHolder.getContext().setAuthentication(null);  
  7.     // 緩存 request  
  8.     requestCache.saveRequest(request, response);  
  9.     logger.debug("Calling Authentication entry point.");  
  10.     authenticationEntryPoint.commence(request, response, reason);  
  11. }  


二、了解了以上原理以及上篇的forward和redirect的區別,配置實現如下,基於springsecurity4.1.3版本

配置文件:完整的

 

[html]  view plain  copy
 
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2.   
  3. <beans:beans xmlns="http://www.springframework.org/schema/security"  
  4.     xmlns:beans="http://www.springframework.org/schema/beans"  
  5.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  6.     xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7.         http://www.springframework.org/schema/beans/spring-beans.xsd  
  8.         http://www.springframework.org/schema/security  
  9.         http://www.springframework.org/schema/security/spring-security.xsd">  
  10.           
  11.     <http auto-config="true" use-expressions="true" entry-point-ref="myLoginUrlAuthenticationEntryPoint">  
  12.         <form-login   
  13.             login-page="/login"  
  14.             authentication-failure-url="/login?error"   
  15.             login-processing-url="/login"  
  16.             authentication-success-handler-ref="myAuthenticationSuccessHandler" />     
  17.          <!-- 認證成功用自定義類myAuthenticationSuccessHandler處理 -->  
  18.            
  19.          <logout logout-url="/logout"   
  20.                 logout-success-url="/"   
  21.                 invalidate-session="true"  
  22.                 delete-cookies="JSESSIONID"/>  
  23.           
  24.         <csrf disabled="true" />  
  25.         <intercept-url pattern="/order/*" access="hasRole('ROLE_USER')"/>  
  26.     </http>  
  27.       
  28.     <!-- 使用自定義類myUserDetailsService從數據庫獲取用戶信息 -->  
  29.     <authentication-manager>    
  30.         <authentication-provider user-service-ref="myUserDetailsService">    
  31.             <!-- 加密 -->  
  32.             <password-encoder hash="md5">  
  33.             </password-encoder>  
  34.         </authentication-provider>  
  35.     </authentication-manager>  
  36.       
  37.     <!-- 被認證請求向登錄界面跳轉采用forward方式 -->  
  38.     <beans:bean id="myLoginUrlAuthenticationEntryPoint"   
  39.         class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">  
  40.         <beans:constructor-arg name="loginFormUrl" value="/login"></beans:constructor-arg>  
  41.         <beans:property name="useForward" value="true"/>  
  42.     </beans:bean>  
  43.       
  44. </beans:beans>  


主要配置

 

 

 

 

[html]  view plain  copy
 
  1. <pre code_snippet_id="1902646" snippet_file_name="blog_20160927_9_7050170" class="html" name="code"><http auto-config="true" use-expressions="true" entry-point-ref="myLoginUrlAuthenticationEntryPoint">  
  2.   
  3.  <!-- 被認證請求向登錄界面跳轉采用forward方式 -->  
  4.     <beans:bean id="myLoginUrlAuthenticationEntryPoint"   
  5.         class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">  
  6.         <beans:constructor-arg name="loginFormUrl" value="/login"></beans:constructor-arg>  
  7.         <beans:property name="useForward" value="true"/>  
  8.     </beans:bean></pre><br>  
  9. <pre></pre>  
  10. <p>從上面的分析可知,默認情況下采用的是redirect方式,這里通過配置從而實現了forward方式,這里還是直接利用的security自帶的類LoginUrlAuthenticationEntryPoint,只不過進行了以上配置:</p>  
  11. <p></p><pre code_snippet_id="1902646" snippet_file_name="blog_20161215_10_1004934" class="java" name="code">/**  
  12.      * Performs the redirect (or forward) to the login form URL.  
  13.      */  
  14.     public void commence(HttpServletRequest request, HttpServletResponse response,  
  15.             AuthenticationException authException) throws IOException, ServletException {  
  16.   
  17.         String redirectUrl = null;  
  18.   
  19.         if (useForward) {  
  20.   
  21.             if (forceHttps && "http".equals(request.getScheme())) {  
  22.                 // First redirect the current request to HTTPS.  
  23.                 // When that request is received, the forward to the login page will be  
  24.                 // used.  
  25.                 redirectUrl = buildHttpsRedirectUrlForRequest(request);  
  26.             }  
  27.   
  28.             if (redirectUrl == null) {  
  29.                 String loginForm = determineUrlToUseForThisRequest(request, response,  
  30.                         authException);  
  31.   
  32.                 if (logger.isDebugEnabled()) {  
  33.                     logger.debug("Server side forward to: " + loginForm);  
  34.                 }  
  35.   
  36.                 RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);  
  37.   
  38.                 dispatcher.forward(request, response);  
  39.   
  40.                 return;  
  41.             }  
  42.         }  
  43.         else {  
  44.             // redirect to login page. Use https if forceHttps true  
  45.   
  46.             redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);  
  47.   
  48.         }  
  49.   
  50.         redirectStrategy.sendRedirect(request, response, redirectUrl);  
  51.     }</pre><br>  
  52. <p></p>  
  53. <p></p>  
  54. <p></p>  
  55. <p></p>  
  56. <p>登錄成功后的類配置,存入登錄user信息后交給認證成功后的處理類MyAuthenticationSuccessHandler,該類集成了SavedRequestAwareAuthenticationSuccessHandler,他會從緩存中提取請求,從而可以恢復之前請求的數據</p>  
  57. <p></p><pre code_snippet_id="1902646" snippet_file_name="blog_20161215_11_4222490" class="java" name="code">/**  
  58.  * 登錄后操作  
  59.  *   
  60.  * @author HHL  
  61.  * @date  
  62.  *   
  63.  */  
  64. @Component  
  65. public class MyAuthenticationSuccessHandler extends  
  66.         SavedRequestAwareAuthenticationSuccessHandler {  
  67.   
  68.     @Autowired  
  69.     private IUserService userService;  
  70.   
  71.     @Override  
  72.     public void onAuthenticationSuccess(HttpServletRequest request,  
  73.             HttpServletResponse response, Authentication authentication)  
  74.             throws IOException, ServletException {  
  75.   
  76.         // 認證成功后,獲取用戶信息並添加到session中  
  77.         UserDetails userDetails = (UserDetails) authentication.getPrincipal();  
  78.         MangoUser user = userService.getUserByName(userDetails.getUsername());  
  79.         request.getSession().setAttribute("user", user);  
  80.           
  81.         super.onAuthenticationSuccess(request, response, authentication);  
  82.       
  83.     }  
  84.   
  85.   
  86. }</pre><p></p>  
  87. <p></p>  
  88. <p></p>  
  89. <p></p>  
  90. <p>SavedRequestAwareAuthenticationSuccessHandler中的onAuthenticationSuccess方法;</p>  
  91. <p></p><pre code_snippet_id="1902646" snippet_file_name="blog_20161215_12_7440047" class="java" name="code">@Override  
  92.     public void onAuthenticationSuccess(HttpServletRequest request,  
  93.             HttpServletResponse response, Authentication authentication)  
  94.             throws ServletException, IOException {  
  95.         SavedRequest savedRequest = requestCache.getRequest(request, response);  
  96.   
  97.         if (savedRequest == null) {  
  98.             super.onAuthenticationSuccess(request, response, authentication);  
  99.   
  100.             return;  
  101.         }  
  102.         String targetUrlParameter = getTargetUrlParameter();  
  103.         if (isAlwaysUseDefaultTargetUrl()  
  104.                 || (targetUrlParameter != null && StringUtils.hasText(request  
  105.                         .getParameter(targetUrlParameter)))) {  
  106.             requestCache.removeRequest(request, response);  
  107.             super.onAuthenticationSuccess(request, response, authentication);  
  108.   
  109.             return;  
  110.         }  
  111.   
  112.         clearAuthenticationAttributes(request);  
  113.   
  114.         // Use the DefaultSavedRequest URL  
  115.         String targetUrl = savedRequest.getRedirectUrl();  
  116.         logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);  
  117.         getRedirectStrategy().sendRedirect(request, response, targetUrl);  
  118.     }</pre>4.1.3中如果默認不配置的話也是采用的SavedRequestAwareAuthenticationSuccessHandler進行處理,詳情可參見:<target="_blank" href="http://blog.csdn.net/honghailiang888/article/details/53541664">Spring實戰篇系列----源碼解析Spring Security中的過濾器Filter初始化  
  119. </a><br>  
  120. <br>  
  121. <br>  
  122. 上述實現了跳轉到登錄界面采用forward方式,就是瀏覽器地址欄沒有變化,當然也可采用redirect方式,地址欄變為登錄界面地址欄,當登錄完成后恢復到原先的請求頁面,請求信息會從requestCache中還原回來。可參考<target="_blank" href="http://blog.csdn.net/honghailiang888/article/details/53520557"> Spring實戰篇系列----spring security4.1.3配置以及踩過的坑</a><br>  
  123. <p></p>  
  124. <p></p>  
  125. <p></p>  
  126. <p></p>  
  127. <p></p>  
  128. <p></p>  
  129. <p><br>  
  130. </p>  
  131. <p><br>  
  132. </p>  
  133. <p>參考:</p>  
  134. <p><target="_blank" href="https://segmentfault.com/a/1190000004183264">https://segmentfault.com/a/1190000004183264</a></p>  
  135. <p><target="_blank" href="http://gtbald.iteye.com/blog/1214132">http://gtbald.iteye.com/blog/1214132</a></p>  
  136. <p><br>  
  137. <br>  
  138. <br>  
  139. </p>  
  140. <div style="top:0px"></div>  
  141. <div style="top:4827px"></div>  
  142. <div style="top:3353px"></div>  
  143.      
 
 http://blog.csdn.net/honghailiang888/article/details/52679264

 


免責聲明!

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



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