1. 程式人生 > >Spring Security4.1.3實現攔截登錄後向登錄頁面跳轉方式(redirect或forward)返回被攔截界面

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

response href tools 當前 錯誤 界面 sets view 鏈接

一、看下內部原理

簡化後的認證過程分為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. // request匹配,則取出,該操作同時會將緩存的request從session中刪除
  4. HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
  5. (HttpServletRequest) request, (HttpServletResponse) response);
  6. // 優先使用緩存的request
  7. chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
  8. response);
  9. }

何時緩存

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

RequestCache

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

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

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. sendStartAuthentication(request, response, chain,
  9. (AuthenticationException) exception);
  10. }
  11. else if (exception instanceof AccessDeniedException) {
  12. if (authenticationTrustResolver.isAnonymous(SecurityContextHolder
  13. .getContext().getAuthentication())) {
  14. logger.debug(
  15. "Access is denied (user is anonymous); redirecting to authentication entry point",
  16. exception);
  17. sendStartAuthentication(
  18. request,
  19. response,
  20. chain,
  21. new InsufficientAuthenticationException(
  22. "Full authentication is required to access this resource"));
  23. }
  24. else {
  25. logger.debug(
  26. "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
  27. exception);
  28. accessDeniedHandler.handle(request, response,
  29. (AccessDeniedException) exception);
  30. }
  31. }
  32. }

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. // 設置403狀態碼
  10. response.setStatus(HttpServletResponse.SC_FORBIDDEN);
  11. // 轉發到errorPage
  12. RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
  13. dispatcher.forward(request, response);
  14. }
  15. else { // 沒有定義errorPage,則返回403狀態碼(Forbidden),以及錯誤信息
  16. response.sendError(HttpServletResponse.SC_FORBIDDEN,
  17. accessDeniedException.getMessage());
  18. }
  19. }
  20. }

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

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

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

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

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