[SpringSecurity5.6.2源码分析二十]:RequestCacheAwareFilter

前言

  • 想象一个场景:
    • 当我们想要在某个论坛访问某个帖子的时候,因为没有认证,此时就会跳转到登录页
    • 然后进行认证,认证完成后就会自动跳转到那个帖子中
  • 而这个功能正是基于RequestCacheAwareFilter来实现了

1. RequestCacheConfigurer

  • RequestCacheConfigurer是RequestCacheAwareFilter对应的配置类,也正是默认开启的配置类之一

1.1 requestCache(...)

  • RequestCacheConfigurer中可供用户调用的只有一个requestCache(...)
java 复制代码
public RequestCacheConfigurer<H> requestCache(RequestCache requestCache) {
   getBuilder().setSharedObject(RequestCache.class, requestCache);
   return this;
}
  • 可以看出就是往SharedObject中注册一个RequestCache
  • RequestCache:在身份认证发生后,缓存当前请求以供后续使用
java 复制代码
public interface RequestCache {
   /**
    * <ul>
    *     <li>
    *          在身份验证发生后,缓存当前请求以供以后使用
    *           比如说:在一个论坛的帖子中,进行回帖,然后因为没有登录,先将回帖的信息保存到请求缓存器中,再重定向到登录页
    *           然后登陆成功后就会获取请求缓存器中上次保存的回帖信息,然后将当前request进行包装,变成一个回帖请求
    *     </li>
    *     <li>
    *         通常发生在{@link org.springframework.security.web.access.ExceptionTranslationFilter}中
    *     </li>
    * </ul>
    */
   void saveRequest(HttpServletRequest request, HttpServletResponse response);

   /**
    * 返回已保存的请求,保留其缓存状态。
    */
   SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);

   /**
    * 如果与当前请求匹配,则返回保存的请求的包装器。保存的请求应该从缓存中删除。
    */
   HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response);

   /**
    * 删除缓存的请求缓存
    */
   void removeRequest(HttpServletRequest request, HttpServletResponse response);

}
  • RequestCache有两个实现,区别就在于将原请求信息保存在哪:
    • CookieRequestCache:
    • HttpSessionRequestCache:
  • 简单的看下HttpSessionRequestCache的saveRequest(...)方法
java 复制代码
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
   //某些请求不允许缓存请求数据
   if (!this.requestMatcher.matches(request)) {
      if (this.logger.isTraceEnabled()) {
         this.logger.trace(
               LogMessage.format("Did not save request since it did not match [%s]", this.requestMatcher));
      }
      return;
   }
   //创建默认保存对象
   DefaultSavedRequest savedRequest = new DefaultSavedRequest(request, this.portResolver);
   //保存到Session中
   if (this.createSessionAllowed || request.getSession(false) != null) {
      request.getSession().setAttribute(this.sessionAttrName, savedRequest);
      if (this.logger.isDebugEnabled()) {
         this.logger.debug(LogMessage.format("Saved request %s to session", savedRequest.getRedirectUrl()));
      }
   }
   else {
      this.logger.trace("Did not save request since there's no session and createSessionAllowed is false");
   }
}
  • 可以看到原请求被保存为DefaultSavedRequest对象
java 复制代码
public DefaultSavedRequest(HttpServletRequest request, PortResolver portResolver) {
   Assert.notNull(request, "Request required");
   Assert.notNull(portResolver, "PortResolver required");
   //添加Cookie
   addCookies(request.getCookies());
   //添加请求头
   Enumeration<String> names = request.getHeaderNames();
   while (names.hasMoreElements()) {
      String name = names.nextElement();
      //某些请求头不需要缓存
      if (HEADER_IF_MODIFIED_SINCE.equalsIgnoreCase(name) || HEADER_IF_NONE_MATCH.equalsIgnoreCase(name)) {
         continue;
      }
      Enumeration<String> values = request.getHeaders(name);
      while (values.hasMoreElements()) {
         this.addHeader(name, values.nextElement());
      }
   }
   //添加环境
   addLocales(request.getLocales());
   //添加参数
   addParameters(request.getParameterMap());
   // Primitives
   this.method = request.getMethod();
   this.pathInfo = request.getPathInfo();
   this.queryString = request.getQueryString();
   this.requestURI = request.getRequestURI();
   this.serverPort = portResolver.getServerPort(request);
   this.requestURL = request.getRequestURL().toString();
   this.scheme = request.getScheme();
   this.serverName = request.getServerName();
   this.contextPath = request.getContextPath();
   this.servletPath = request.getServletPath();
}

1.2 init(...)

  • 没什么好说的,就是确保SharedObject中会有一个RequestCache

    java 复制代码
        @Override
        public void init(H http) {
           http.setSharedObject(RequestCache.class, getRequestCache(http));
        }
    
        /**
         * 获得请求缓存器
         */
        private RequestCache getRequestCache(H http) {
           //先尝试从sharedObjects中获取
           RequestCache result = http.getSharedObject(RequestCache.class);
           if (result != null) {
              return result;
           }
           //尝试从容器中获取
           result = getBeanOrNull(RequestCache.class);
           if (result != null) {
              return result;
           }
           //还是没有,就创建一个基于HttpSession的请求缓冲器
           HttpSessionRequestCache defaultCache = new HttpSessionRequestCache();
           defaultCache.setRequestMatcher(createDefaultSavedRequestMatcher(http));
           return defaultCache;
        }
    • 但是这里有一个重点就是有哪些请求才需要被保存呢,这就需要注册指定的请求匹配器(RequestMatcher)
    • 下图就是RequestCacheConfigurer中默认注册的请求匹配器了
    java 复制代码
    private RequestMatcher createDefaultSavedRequestMatcher(H http) {
       //第一个:不缓存路径为/**/favicon.*的请求
       RequestMatcher notFavIcon = new NegatedRequestMatcher(new AntPathRequestMatcher("/**/favicon.*"));
       //第二个:不缓存异步请求
       RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
             new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
       boolean isCsrfEnabled = http.getConfigurer(CsrfConfigurer.class) != null;
       List<RequestMatcher> matchers = new ArrayList<>();
       //如果开启了Csrf的保护
       if (isCsrfEnabled) {
          //第三个:为了安全考虑,只能缓存GET方式的请求
          RequestMatcher getRequests = new AntPathRequestMatcher("/**", "GET");
          matchers.add(0, getRequests);
       }
       matchers.add(notFavIcon);
       //第四个:不缓存媒体类型为 application/json 的请求
       matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON));
       matchers.add(notXRequestedWith);
       //第四个:不缓存媒体类型为 multipart/form-data 的请求
       matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA));
       //第四个:不缓存媒体类型为 text/event-stream 的请求
       matchers.add(notMatchingMediaType(http, MediaType.TEXT_EVENT_STREAM));
       return new AndRequestMatcher(matchers);
    }
    • 要注意的是这里注册的是一个AndRequestMatcher,也就说请求需要满足内部的所有RequestMatcher才算匹配成功

1.3 configure(...)

  • configure(...):注册过滤器

    java 复制代码
    @Override
    public void configure(H http) {
       //获得请求缓存器
       RequestCache requestCache = getRequestCache(http);
       //创建对应过滤器
       RequestCacheAwareFilter requestCacheFilter = new RequestCacheAwareFilter(requestCache);
       requestCacheFilter = postProcess(requestCacheFilter);
       http.addFilter(requestCacheFilter);
    }
    
    /**
     * 获得请求缓存器
     */
    private RequestCache getRequestCache(H http) {
       //先尝试从sharedObjects中获取
       RequestCache result = http.getSharedObject(RequestCache.class);
       if (result != null) {
          return result;
       }
       //尝试从容器中获取
       result = getBeanOrNull(RequestCache.class);
       if (result != null) {
          return result;
       }
       //还是没有,就创建一个基于HttpSession的请求缓冲器
       HttpSessionRequestCache defaultCache = new HttpSessionRequestCache();
       defaultCache.setRequestMatcher(createDefaultSavedRequestMatcher(http));
       return defaultCache;
    }

2. ExceptionTranslationFilter

  • 前面说了RequestCacheAwareFilter是当用户未认证的情况下,将原请求保存起来供后续认证完成后使用的

  • 所以说就一定有一个地方是判断认证失败了的,然后将请求保存起来的,这个地方就是ExceptionTranslationFilter

  • ExceptionTranslationFilter:是专门用于处理FilterSecurityInterceptor抛出的两大异常

    • 认证异常:AuthenticationException
    • 访问被拒绝异常:AccessDeniedException
  • 在对应配置类的configure(...)方法中从SharedObject中获得了RequestCache并将其保存在ExceptionTranslationFilter中

    java 复制代码
    @Override
    public void configure(H http) {
       ...
       //创建处理异常的过滤器,还传入了请求缓存器
       ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
             getRequestCache(http));
       ...
    }
    
    /**
     * 重点:从SharedObject获得RequestCache
     */
    private RequestCache getRequestCache(H http) {
       RequestCache result = http.getSharedObject(RequestCache.class);
       if (result != null) {
          return result;
       }
       return new HttpSessionRequestCache();
    }
  • 而在ExceptionTranslationFilter的sendStartAuthentication(...)方法中,就将原请求保存起来了

    java 复制代码
    /**
     * 处理认证异常
     */
    protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
          AuthenticationException reason) throws ServletException, IOException {
       //清除存储在线程级别的上下文策略的认证信息,HttpSession级别的会在SecurityContextPersistenceFilter的finally代码块中被更新
       //因为现有的认证不再被认为有效
       SecurityContext context = SecurityContextHolder.createEmptyContext();
       SecurityContextHolder.setContext(context);
       //将当前的请求放入请求缓存器
       //这样当重新登录后,还能将请求包装为这一次请求
       this.requestCache.saveRequest(request, response);
       //执行认证异常处理器
       this.authenticationEntryPoint.commence(request, response, reason);
    }

3 RequestCacheAwareFilter

  • 此过滤器的源码较少, 直接看doFilter(...)方法
  • 是直接包装当前请求,转换为认证前的请求
java 复制代码
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
   //从请求缓冲器中获得上一次请求是数据,并重写包装为一个新的HttpServletRequest
   HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request,
         (HttpServletResponse) response);
   chain.doFilter((wrappedSavedRequest != null) ? wrappedSavedRequest : request, response);
}
  • 最后再讲讲在SpringSecurity中RequestCache机制的完整逻辑
    • 用户访问/A接口,没有对应的权限就会被ExceptionTranslationFilter包装当前请求
      • /A接口:需要认证或者说当前用户没有权限的接口
    • 然后会被身份认证入口点(AuthenticationEntryPoint)重定向到登录页
    • 用户在登录页输入用户名和密码向服务器发起认证请求
    • 紧接着在UsernamePasswordAuthenticationFilter中完成认证后,到达RequestCacheAwareFilter,并将当前请求包装为原请求也就是/A请求
      • 注意此时的请求路径已经由/login变为了/A,而此时就会进入SpringMVC的DispatcherServlet中
    • 然后我们看专门处理@RequestMapping的RequestMappingHandlerMapping中是如何获取处理方法的
  • 没错正是根据请求路径/A获取对应的处理方法
相关推荐
王码码20358 小时前
Go语言的测试:从单元测试到集成测试
后端·golang·go·接口
王码码20358 小时前
Go语言中的测试:从单元测试到集成测试
后端·golang·go·接口
嵌入式×边缘AI:打怪升级日志9 小时前
使用JsonRPC实现前后台
前端·后端
小码哥_常9 小时前
从0到1:Spring Boot 中WebSocket实战揭秘,开启实时通信新时代
后端
lolo大魔王10 小时前
Go语言的异常处理
开发语言·后端·golang
IT_陈寒12 小时前
Python多进程共享变量那个坑,我差点没爬出来
前端·人工智能·后端
码事漫谈12 小时前
2026软考高级·系统架构设计师备考指南
后端
AI茶水间管理员13 小时前
如何让LLM稳定输出 JSON 格式结果?
前端·人工智能·后端
其实是白羊13 小时前
我用 Vibe Coding 搓了一个 IDEA 插件,复制URI 再也不用手动拼了
后端·intellij idea
用户83562907805113 小时前
Python 操作 Word 文档节与页面设置
后端·python