您好,登录后才能下订单哦!
为什么要缓存?
为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为7步:
从第3步,我们可以知道,用户的请求被中断了。
用户登录成功后(第7步),会被重定向到origin url,spring security通过使用缓存的request,使得被中断的请求能够继续执行。
使用缓存
用户登录成功后,页面重定向到origin url。浏览器发出的请求优先被拦截器RequestCacheAwareFilter拦截,RequestCacheAwareFilter通过其持有的RequestCache对象实现request的恢复。
public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    // request匹配,则取出,该操作同时会将缓存的request从session中删除
    HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
        (HttpServletRequest) request, (HttpServletResponse) response);
    // 优先使用缓存的request
    chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
        response);
  }
何时缓存
首先,我们需要了解下RequestCache以及ExceptionTranslationFilter。
RequestCache
RequestCache接口声明了缓存与恢复操作。默认实现类是HttpSessionRequestCache。HttpSessionRequestCache的实现比较简单,这里只列出接口的声明:
public interface RequestCache {
  // 将request缓存到session中
  void saveRequest(HttpServletRequest request, HttpServletResponse response);
  // 从session中取request
  SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
  // 获得与当前request匹配的缓存,并将匹配的request从session中删除
  HttpServletRequest getMatchingRequest(HttpServletRequest request,
      HttpServletResponse response);
  // 删除缓存的request
  void removeRequest(HttpServletRequest request, HttpServletResponse response);
}
ExceptionTranslationFilter
ExceptionTranslationFilter 是Spring Security的核心filter之一,用来处理AuthenticationException和AccessDeniedException两种异常。
在我们的例子中,AuthenticationException指的是未登录状态下访问受保护资源,AccessDeniedException指的是登陆了但是由于权限不足(比如普通用户访问管理员界面)。
ExceptionTranslationFilter 持有两个处理类,分别是AuthenticationEntryPoint和AccessDeniedHandler。
ExceptionTranslationFilter 对异常的处理是通过这两个处理类实现的,处理规则很简单:
对应以下代码
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);
      }
    }
  }
AccessDeniedHandler 默认实现是 AccessDeniedHandlerImpl。该类对异常的处理是返回403错误码。
public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException,
      ServletException {
  if (!response.isCommitted()) {
    if (errorPage != null) { // 定义了errorPage
      // errorPage中可以操作该异常
      request.setAttribute(WebAttributes.ACCESS_DENIED_403,
          accessDeniedException);
      // 设置403状态码
      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
      // 转发到errorPage
      RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
      dispatcher.forward(request, response);
    }
    else { // 没有定义errorPage,则返回403状态码(Forbidden),以及错误信息
      response.sendError(HttpServletResponse.SC_FORBIDDEN,
          accessDeniedException.getMessage());
    }
  }
}
AuthenticationEntryPoint 默认实现是 LoginUrlAuthenticationEntryPoint, 该类的处理是转发或重定向到登录页面
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);
}
了解完这些,回到我们的例子。
第3步时,用户未登录的情况下访问受保护资源,ExceptionTranslationFilter会捕获到AuthenticationException异常(规则1)。页面需要跳转,ExceptionTranslationFilter在跳转前使用requestCache缓存request。
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);
  // 缓存 request
  requestCache.saveRequest(request, response);
  logger.debug("Calling Authentication entry point.");
  authenticationEntryPoint.commence(request, response, reason);
}
一些坑
在开发过程中,如果不理解Spring Security如何缓存request,可能会踩一些坑。
举个简单例子,如果网站认证是信息存放在header中。第一次请求受保护资源时,请求头中不包含认证信息 ,验证失败,该请求会被缓存,之后即使用户填写了信息,也会因为request被恢复导致信息丢失从而认证失败(问题描述可以参见这里。
最简单的方案当然是不缓存request。
spring security 提供了NullRequestCache, 该类实现了 RequestCache 接口,但是没有任何操作。
public class NullRequestCache implements RequestCache {
  public SavedRequest getRequest(HttpServletRequest request,
      HttpServletResponse response) {
    return null;
  }
  public void removeRequest(HttpServletRequest request, HttpServletResponse response) {
  }
  public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
  }
  public HttpServletRequest getMatchingRequest(HttpServletRequest request,
      HttpServletResponse response) {
    return null;
  }
}
配置requestCache,使用如下代码即可:
http.requestCache().requestCache(new NullRequestCache());
补充
默认情况下,三种request不会被缓存。
可以参见:RequestCacheConfigurer类中的私有方法createDefaultSavedRequestMatcher。
附上实例代码: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache
以上所述是小编给大家介绍的Spring Security 缓存请求问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对亿速云网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。