Spring Security4.1.3实现拦截登录后向登录页面跳转方式(redirect或forward)返回被拦截界面

时间:2023-12-17 16:07:20

一、看下内部原理

简化后的认证过程分为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的恢复。

  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的实现比较简单,这里只列出接口的声明:

  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 对异常的处理是通过这两个处理类实现的,处理规则很简单:

  1. 规则1. 如果异常是 AuthenticationException,使用 AuthenticationEntryPoint 处理
  2. 规则2. 如果异常是 AccessDeniedException 且用户是匿名用户,使用 AuthenticationEntryPoint 处理
  3. 规则3. 如果异常是 AccessDeniedException 且用户不是匿名用户,如果否则交给 AccessDeniedHandler 处理。

对应以下代码

  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错误码。

  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, 该类的处理是转发或重定向到登录页面

  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。

  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版本

配置文件:完整的

  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>

主要配置

  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