An in depth analysis of Spring Security cache request

  • 2021-07-18 08:07:01
  • OfStack

Why cache?

In order to better describe the problem, we take the website using form authentication as an example. The simplified authentication process is divided into 7 steps:

The user visits the website and opens a link (origin url). The request is sent to the server, which determines that the user requested the protected resource. Because the user is not logged in, the server redirects to the login page Fill out the form and click Login The browser sends the user name and password to the server in the form of a form The server verifies the username and password. Success, go to the next step. Otherwise, the user is required to re-authenticate (Step 3) Server to the user has the authority (role) judgment: has the authority, redirects to origin url; Insufficient permissions, return status code 403 ("forbidden").

From Step 3, we can know that the user's request was interrupted.

After the user logs in successfully (step 7), it is redirected to origin url, and spring security enables the interrupted request to continue by using the cached request.

Using cache

After the user logs in successfully, the page is redirected to origin url. The request from the browser is first intercepted by the interceptor RequestCacheAwareFilter, and RequestCacheAwareFilter realizes the recovery of request through the RequestCache object held by RequestCacheAwareFilter.


public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException, ServletException {

    // request Match, then fetch, which will also change the cached request From session Delete in 
    HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
        (HttpServletRequest) request, (HttpServletResponse) response);

    //  Preferential use of cached request
    chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
        response);
  }

When to cache

First, we need to know about RequestCache and ExceptionTranslationFilter.

RequestCache

The RequestCache interface declares caching and recovery operations. The default implementation class is HttpSessionRequestCache. The implementation of HttpSessionRequestCache is relatively simple, and only the declaration of the interface is listed here:


public interface RequestCache {
  //  Will request Cache to session Medium 
  void saveRequest(HttpServletRequest request, HttpServletResponse response);
  //  From session Take from the middle request
  SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
  //  Obtain and current request Matches the cache, and sets the matching cache request From session Delete in 
  HttpServletRequest getMatchingRequest(HttpServletRequest request,
      HttpServletResponse response);
  //  Delete the cached request
  void removeRequest(HttpServletRequest request, HttpServletResponse response);
}

ExceptionTranslationFilter

ExceptionTranslationFilter is one of the core filter of Spring Security, which is used to handle AuthenticationException and AccessDeniedException exceptions.

In our example, AuthenticationException refers to accessing protected resources without logging in, and AccessDeniedException refers to logging in but due to insufficient permissions (such as ordinary users accessing the administrator interface).

ExceptionTranslationFilter holds two processing classes, AuthenticationEntryPoint and AccessDeniedHandler.

ExceptionTranslationFilter handles exceptions through these two handling classes, and the handling rules are simple:

Rule 1. If the exception is AuthenticationException, use AuthenticationEntryPoint to handle Rule 2. If the exception is AccessDeniedException and the user is anonymous, use AuthenticationEntryPoint to handle Rule 3. If the exception is an AccessDeniedException and the user is not an anonymous user, leave it to AccessDeniedHandler if not.

Corresponds to the following code


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);
      }
    }
  }

The default implementation of AccessDeniedHandler is AccessDeniedHandlerImpl. This class handles exceptions by returning 403 error codes.


public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException,
      ServletException {
  if (!response.isCommitted()) {
    if (errorPage != null) { //  Defines the errorPage
      // errorPage You can manipulate the exception in 
      request.setAttribute(WebAttributes.ACCESS_DENIED_403,
          accessDeniedException);
      //  Settings 403 Status code 
      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
      //  Forward to errorPage
      RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
      dispatcher.forward(request, response);
    }
    else { //  There is no definition errorPage Is returned 403 Status code (Forbidden) And error messages 
      response.sendError(HttpServletResponse.SC_FORBIDDEN,
          accessDeniedException.getMessage());
    }
  }
}

The default implementation of AuthenticationEntryPoint is LoginUrlAuthenticationEntryPoint, and the processing of this class is forwarding or redirecting to the login page


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);
      //  Forwarding 
      dispatcher.forward(request, response);
      return;
    }
  }
  else {
    // redirect to login page. Use https if forceHttps true
    redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
  }
  //  Redirect 
  redirectStrategy.sendRedirect(request, response, redirectUrl);
}

After understanding this, go back to our example.

In Step 3, ExceptionTranslationFilter catches an AuthenticationException exception when the user accesses a protected resource without logging in (Rule 1). The page needs to jump, and ExceptionTranslationFilter uses requestCache to cache request before jumping.


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);
  //  Cache  request
  requestCache.saveRequest(request, response);
  logger.debug("Calling Authentication entry point.");
  authenticationEntryPoint.commence(request, response, reason);
}

1 Some pits

During development, if you don't understand how Spring Security caches request, you may step into a hole.

To give a simple example, if the website authentication is that the information is stored in header. When a protected resource is requested for the first time, the authentication information is not contained in the request header. If the authentication fails, the request will be cached. After that, even if the user fills in the information, the authentication will fail because the information is lost due to the recovery of request (see here for the description of the problem).

The simplest solution is, of course, not to cache request.

spring security provides NullRequestCache, which implements the RequestCache interface but does not operate.


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;
  }
}

To configure requestCache, use the following code:


http.requestCache().requestCache(new NullRequestCache());

Supplement

By default, the three request are not cached.

The request address ends with/favicon. ico The content-type value in header is application/json The X-Requested-With value in header is XMLHttpRequest

See the private method createDefaultSavedRequestMatcher in the RequestCacheConfigurer class.

Attached is the example code: https://coding.net/u/tanhe123/p/SpringSecurityRequestCache


Related articles: