[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获取对应的处理方法
相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
2401_857610034 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
颜淡慕潇6 小时前
【K8S问题系列 | 9】如何监控集群CPU使用率并设置告警?
后端·云原生·容器·kubernetes·问题解决
CoderJia程序员甲6 小时前
重学SpringBoot3-整合 Elasticsearch 8.x (三)使用Repository
java·大数据·spring boot·elasticsearch
荆州克莱6 小时前
Mysql学习笔记(一):Mysql的架构
spring boot·spring·spring cloud·css3·技术
独泪了无痕6 小时前
WebStorm 如何调试 Vue 项目
后端·webstorm
怒放吧德德7 小时前
JUC从实战到源码:JMM总得认识一下吧
java·jvm·后端
代码小鑫8 小时前
A025-基于SpringBoot的售楼管理系统的设计与实现
java·开发语言·spring boot·后端·毕业设计
前端SkyRain8 小时前
后端SpringBoot学习项目-项目基础搭建
spring boot·后端·学习