当前位置 : 主页 > 编程语言 > java >

深入浅析 Spring Security 缓存请求问题

来源:互联网 收集:自由互联 发布时间:2021-07-07
为什么要缓存 为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为7步: 用户访问网站,打开了一个链接(origin url)。 请求发送给服务器,服务器判断用户请求

为什么要缓存?

为了更好的描述问题,我们拿使用表单认证的网站举例,简化后的认证过程分为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的恢复。

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

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

对应以下代码

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不会被缓存。

  1. 请求地址以/favicon.ico结尾
  2. header中的content-type值为application/json
  3. header中的X-Requested-With值为XMLHttpRequest

可以参见:RequestCacheConfigurer类中的私有方法createDefaultSavedRequestMatcher。

附上实例代码: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache

以上所述是小编给大家介绍的Spring Security 缓存请求问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对自由互联网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

网友评论