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

前言

  • FilterSecurityInterceptor 作为SpringSecurity中负责进行鉴权的过滤器之一
    • 另外一个是MethodSecurityInterceptor

1. ExpressionUrlAuthorizationConfigurer

  • FilterSecurityInterceptor对应的配置类有两个,如下
    • ExpressionUrlAuthorizationConfigurer
    • UrlAuthorizationConfigurer
  • SpringSecurity默认的配置类是:ExpressionUrlAuthorizationConfigurer
  • 此配置类不是默认开启的,需要通过下述的代码手动开启
java 复制代码
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
   }
  • authorizeRequests()的本质上是返回一个基于表达式的 Url注册中心
java 复制代码
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()
      throws Exception {
   ApplicationContext context = getContext();
   return getOrApply(new ExpressionUrlAuthorizationConfigurer<>(context))
         .getRegistry();
}

1.1 antMatchers(...)

  • 要理解这个方法要先明白:权限校验的本质上是规定访问某个方法或某个接口需要什么样的权限
  • 而antMatchers(...)的本质上就是创建请求匹配器RequestMatcher
java 复制代码
public C antMatchers(String... antPatterns) {
   Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
   return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
}

static List<RequestMatcher> antMatchers(String... antPatterns) {
   return antMatchers(null, antPatterns);
}

/**
 * 根据传入的方法和路径创建 AntPathRequestMatcher
 */
static List<RequestMatcher> antMatchers(HttpMethod httpMethod, String... antPatterns) {
   String method = (httpMethod != null) ? httpMethod.toString() : null;
   List<RequestMatcher> matchers = new ArrayList<>();
   for (String pattern : antPatterns) {
      matchers.add(new AntPathRequestMatcher(pattern, method));
   }
   return matchers;
}
  • 而这些请求匹配器又会被包装为AuthorizedUrl ,并返回
    • AuthorizedUrl:保存请求匹配器的地方,方便后续为这些请求匹配器指定规则
  • 当获得了创建了请求匹配器的之后我们就需要通过AuthorizedUrl创建对应的权限规则了
  • 比如
    • hasRole("/amdin"):需要admin角色
    • hasAuthority("ROLE_admin"):需要ROLE_admin权限
      • 这里的权限和角色没有区别,只不过权限名前要加ROLE_而已
    • fullyAuthenticated():需要完整认证,指的是不通过记住我用户和匿名用户
    • authenticated():任何通过身份认证的用户都允许访问
    • anonymous():匿名用户方可访问
    • ...
  • 上述的所有方法的本质上都是执行access(...)方法
    • access(...):将请求匹配器和对应的权限规则封装为UrlMapping,并注册起来
java 复制代码
public ExpressionInterceptUrlRegistry access(String attribute) {
   //是否已经否定之后的所有规则
   if (this.not) {
      attribute = "!" + attribute;
   }
   interceptUrl(this.requestMatchers, SecurityConfig.createList(attribute));
   return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
}

/**
 * 注册关联关系
 */
private void interceptUrl(Iterable<? extends RequestMatcher> requestMatchers,
      Collection<ConfigAttribute> configAttributes) {
   for (RequestMatcher requestMatcher : requestMatchers) {
      REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(
            requestMatcher, configAttributes));
   }
}

1.2 mvcMatchers(...)

  • 和antMatchers(...)一样,都是为了创建请求匹配器的,但是创建的请求匹配器是MvcRequestMatcher
  • MvcRequestMatcher: 使用Spring MVC的HandlerMappingIntrospector来匹配路径和提取变量
    • 是会匹配Url+请求方式的
    • 如果请求Url是/a, 与AntPathRequestMatcher的区别在于这个还能匹配/a.html,/a.abc之类的
java 复制代码
public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtractor {

   private final DefaultMatcher defaultMatcher = new DefaultMatcher();

   /**
    * Spring MVC的 HandlerMappingIntrospector
    * 里面有全部的HandlerMapping
    */
   private final HandlerMappingIntrospector introspector;

   /**
    * 匹配的请求url
    */
   private final String pattern;

   /**
    * 请求方式
    */
   private HttpMethod method;

   private String servletPath;

   public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) {
      this.introspector = introspector;
      this.pattern = pattern;
   }

   @Override
   public boolean matches(HttpServletRequest request) {
      //判断请求方式和servletPath是否相同
      if (notMatchMethodOrServletPath(request)) {
         return false;
      }
      MatchableHandlerMapping mapping = getMapping(request);
      if (mapping == null) {
         return this.defaultMatcher.matches(request);
      }
      RequestMatchResult matchResult = mapping.match(request, this.pattern);
      //必须要存在这个url对应的方法
      return matchResult != null;
   }
   ...
}

1.3 regexMatchers(...)

  • 和mvcMatchers(...)一样都很特殊,这里创建的请求匹配器为RegexRequestMatcher
  • RegexRequestMatcher:基于Url正则表达式和请求方式的请求匹配器
    • 可以配置为匹配特定的HTTP方法
    • 可以针对请求的servletPath + pathInfo + queryString执行的
    • 默认情况下是区分大小写的

1.4 expressionHandler()

  • expressionHandler():注册SecurityExpressionHandler
java 复制代码
public ExpressionInterceptUrlRegistry expressionHandler(
      SecurityExpressionHandler<FilterInvocation> expressionHandler) {
   ExpressionUrlAuthorizationConfigurer.this.expressionHandler = expressionHandler;
   return this;
}
  • SecurityExpressionHandler:是用于解析和评估安全表达式的接口。它提供了一种扩展机制,允许自定义安全表达式的解析方式
java 复制代码
public interface SecurityExpressionHandler<T> extends AopInfrastructureBean {

   /**
    * @return an expression parser for the expressions used by the implementation.
    */
   ExpressionParser getExpressionParser();

   /**
    * Provides an evaluation context in which to evaluate security expressions for the
    * invocation type.
    */
   EvaluationContext createEvaluationContext(Authentication authentication, T invocation);

}

1.5 accessDecisionManager(...)

  • accessDecisionManager(...):注册访问决策管理器(AccessDecisionManager)
java 复制代码
public R accessDecisionManager(AccessDecisionManager accessDecisionManager) {
   AbstractInterceptUrlConfigurer.this.accessDecisionManager = accessDecisionManager;
   return getSelf();
}
  • AccessDecisionManager:根据访问决策投票器(AccessDecisionVoter)的投票数决定最终的鉴权结果
java 复制代码
public interface AccessDecisionManager {

   void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
         throws AccessDeniedException, InsufficientAuthenticationException;

   /**
    * 是否支持解析指定的权限
    */
   boolean supports(ConfigAttribute attribute);

    */
   boolean supports(Class<?> clazz);

}
  • 此类有三个实现:
    • AffirmativeBased:只要有任何一个投票器投了同意一票,就允许
    • ConsensusBased:根据大多数投票器的决定来确定返回
    • UnanimousBased:所有投票器都必须投同意票
  • 此类的重点就是调用访问决策投票器(AccessDecisionVoter)进行投票
  • 然后我们再看访问决策投票器(AccessDecisionVoter)的实现类:
    • RoleVoter:用于判断认证对象是否有任何一个要求的角色,就投同意票
    • RoleHierarchyVoter :与上面的区别在于内部使用了角色继承器(RoleHierarchy)来获取用户的权限
    • WebExpressionVoter:基于表达式权限控制的投票器
    • AuthenticatedVoter :根据认证方式投票
      • 比如说要求完整认证,记住我认证,匿名认证
    • ...

1.6 常用方法

  • filterSecurityInterceptorOncePerRequest(...):FilterSecurityInterceptor过滤器可能会执行多次,那么这个就是决定后续执行此过滤器是否需要跳过权限检查
  • anyRequest(): 和antMatchers(...)一样,都是为了创建请求匹配器的,只不过这里创建的是一个能够匹配所有请求的

1.7 configure(...)

  • configure(...)的核心是创建了安全元数据源(FilterInvocationSecurityMetadataSource)
java 复制代码
public void configure(H http) throws Exception {
   //获得安全元数据
   FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
   if (metadataSource == null) {
      return;
   }
   //创建对应过滤器
   FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(http, metadataSource,
         http.getSharedObject(AuthenticationManager.class));


   // 是否跳过重复的认证检查
   if (this.filterSecurityInterceptorOncePerRequest != null) {
      securityInterceptor.setObserveOncePerRequest(this.filterSecurityInterceptorOncePerRequest);
   }

   securityInterceptor = postProcess(securityInterceptor);
   http.addFilter(securityInterceptor);
   http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
}
  • createMetadataSource(...):将我们通过antMatchers(...)等方法注册的关联关系包装为ExpressionBasedFilterInvocationSecurityMetadataSource
java 复制代码
ExpressionBasedFilterInvocationSecurityMetadataSource createMetadataSource(H http) {
   //获得请求表达式和权限表达式的映射关系
   LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = this.REGISTRY.createRequestMap();
   Assert.state(!requestMap.isEmpty(),
         "At least one mapping is required (i.e. authorizeRequests().anyRequest().authenticated())");
   return new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap, getExpressionHandler(http));
}
  • getExpressionHandler(...):获得Web安全表达式处理器
java 复制代码
private SecurityExpressionHandler<FilterInvocation> getExpressionHandler(H http) {
   if (this.expressionHandler != null) {
      return this.expressionHandler;
   }
   DefaultWebSecurityExpressionHandler defaultHandler = new DefaultWebSecurityExpressionHandler();

   //设置认证对象分析器
   AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
   if (trustResolver != null) {
      defaultHandler.setTrustResolver(trustResolver);
   }
   ApplicationContext context = http.getSharedObject(ApplicationContext.class);
   if (context != null) {
      //设置角色继承器
      String[] roleHiearchyBeanNames = context.getBeanNamesForType(RoleHierarchy.class);
      if (roleHiearchyBeanNames.length == 1) {
         defaultHandler.setRoleHierarchy(context.getBean(roleHiearchyBeanNames[0], RoleHierarchy.class));
      }

      //设置角色前缀
      String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
      if (grantedAuthorityDefaultsBeanNames.length == 1) {
         GrantedAuthorityDefaults grantedAuthorityDefaults = context
               .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class);
         defaultHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
      }

      //设置权限评估器
      String[] permissionEvaluatorBeanNames = context.getBeanNamesForType(PermissionEvaluator.class);
      if (permissionEvaluatorBeanNames.length == 1) {
         PermissionEvaluator permissionEvaluator = context.getBean(permissionEvaluatorBeanNames[0],
               PermissionEvaluator.class);
         defaultHandler.setPermissionEvaluator(permissionEvaluator);
      }
   }
   this.expressionHandler = postProcess(defaultHandler);
   return this.expressionHandler;
}

1.7.1 FilterInvocationSecurityMetadataSource

  • FilterInvocationSecurityMetadataSource是一个空类,核心方法在SecurityMetadataSource上
  • 这个类的重点就是通过getAttributes(...)方法获取访问此资源所需要的权限表达式
java 复制代码
public interface SecurityMetadataSource extends AopInfrastructureBean {

   /**
    * 根据传入的对象返回 {@link ConfigAttribute}
    */
   Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;

   /**
    * 返回所有的 {@link ConfigAttribute}
    */
   Collection<ConfigAttribute> getAllConfigAttributes();

   boolean supports(Class<?> clazz);

}
  • 我们看他的默认实现:DefaultFilterInvocationSecurityMetadataSource
java 复制代码
public Collection<ConfigAttribute> getAttributes(Object object) {
   final HttpServletRequest request = ((FilterInvocation) object).getRequest();
   int count = 0;
   for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : this.requestMap.entrySet()) {
      if (entry.getKey().matches(request)) {
         return entry.getValue();
      }
      else {
         if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Did not match request to %s - %s (%d/%d)", entry.getKey(),
                  entry.getValue(), ++count, this.requestMap.size()));
         }
      }
   }
   return null;
}

2. FilterSecurityInterceptor

  • doFilter(...)方法直接调用了invoke(...)方法,所以我们直接看此方法
java 复制代码
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
   //确定是否跳过安全检查
   if (isApplied(filterInvocation) && this.observeOncePerRequest) {
      // filter already applied to this request and user wants us to observe
      // once-per-request handling, so don't re-do security checking
      filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
      return;
   }
   //判断此请求是否是第一次调用此过滤器
   if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
      //标记为已经执行过安全检查了
      filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
   }
   //执行调用前的权限判断
   InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
   try {
      filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
   }
   finally {
      //是否需要将认证对象恢复到 判断权限之前
      super.finallyInvocation(token);
   }
   //执行调用后的权限判断
   super.afterInvocation(token, null);
}
  • 此方法有三个重点:
    • super.beforeInvocation(filterInvocation):执行调用前的权限判断
    • super.finallyInvocation(token):是否需要将认证对象恢复到 判断权限之前
    • super.afterInvocation(token, null):执行调用后的权限判断
  • 我们先看后面两个重点
  • super.finallyInvocation(token):是否需要将认证对象恢复到 判断权限之前
java 复制代码
protected void finallyInvocation(InterceptorStatusToken token) {
   /*
      这个if成立的情况一般都是RunAsUserToken的认证对象的情况
      由于RunAsUserToken是新增了一些权限的,所以需要刷新回去
    */
   if (token != null && token.isContextHolderRefreshRequired()) {
      SecurityContextHolder.setContext(token.getSecurityContext());
      if (this.logger.isDebugEnabled()) {
         this.logger.debug(LogMessage.of(
               () -> "Reverted to original authentication " + token.getSecurityContext().getAuthentication()));
      }
   }
}
  • super.afterInvocation(token, null):此方法是配合@PostAuthorize等后置注解使用的,这里不会用到
    • 但是在MethodSecurityInterceptor中会有使用,后续在说
java 复制代码
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
   //如果token为空,就说接口是一个公共接口,不需要权限,就直接返回
   if (token == null) {
      return returnedObject;
   }
   //是否需要将认证对象恢复到 判断权限之前
   finallyInvocation(token);
   if (this.afterInvocationManager != null) {
      try {
         //处理之后目标方法后的操作
         returnedObject = this.afterInvocationManager.decide(token.getSecurityContext().getAuthentication(),
               token.getSecureObject(), token.getAttributes(), returnedObject);
      }
      catch (AccessDeniedException ex) {
         //发布授权失败异常
         publishEvent(new AuthorizationFailureEvent(token.getSecureObject(), token.getAttributes(),
               token.getSecurityContext().getAuthentication(), ex));
         throw ex;
      }
   }
   return returnedObject;
}
  • 现在只剩下一个重点:super.beforeInvocation(filterInvocation);
java 复制代码
protected InterceptorStatusToken beforeInvocation(Object object) {
   Assert.notNull(object, "Object was null");
   //不懂具体应用场景
   if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
      throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
            + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
            + getSecureObjectClass());
   }
   //通过安全元数据获得接口所需权限
   Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);

   //当不需要任何权限的时候
   if (CollectionUtils.isEmpty(attributes)) {
      Assert.isTrue(!this.rejectPublicInvocations,
            () -> "Secure object invocation " + object
                  + " was denied as public invocations are not allowed via this interceptor. "
                  + "This indicates a configuration error because the "
                  + "rejectPublicInvocations property is set to 'true'");
      if (this.logger.isDebugEnabled()) {
         this.logger.debug(LogMessage.format("Authorized public object %s", object));
      }
      //发布特定事件
      publishEvent(new PublicInvocationEvent(object));
      return null;
   }

   //确定有认证对象
   if (SecurityContextHolder.getContext().getAuthentication() == null) {
      //发布认证对象未找到事件
      credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
            "An Authentication object was not found in the SecurityContext"), object, attributes);
   }
   //获得认对象
   Authentication authenticated = authenticateIfRequired();
   if (this.logger.isTraceEnabled()) {
      this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
   }
   // 调用访问决策管理器进行权限判断
   attemptAuthorization(object, attributes, authenticated);
   if (this.logger.isDebugEnabled()) {
      this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
   }

   //发布特定的事件
   if (this.publishAuthorizationSuccess) {
      publishEvent(new AuthorizedEvent(object, attributes, authenticated));
   }

   //尝试以不同的用户执行
   Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);

   //更新线程级别上下文策略中的认证对象
   if (runAs != null) {
      SecurityContext origCtx = SecurityContextHolder.getContext();
      SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
      newCtx.setAuthentication(runAs);
      SecurityContextHolder.setContext(newCtx);

      if (this.logger.isDebugEnabled()) {
         this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
      }
      //第二个参数表示:需要恢复认证对象
      return new InterceptorStatusToken(origCtx, true, attributes, object);
   }
   this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
   //第二个参数表示:需要不需要恢复认证对象
   return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

}
  • 绝大部分代码都已经写了注释了,其中只有一个RunAsManager是陌生的类
  • RunAsManager:为当前的认证对象创建临时的认证对象
java 复制代码
public interface RunAsManager {

   Authentication buildRunAs(Authentication authentication, Object object, Collection<ConfigAttribute> attributes);

   boolean supports(ConfigAttribute attribute);

   boolean supports(Class<?> clazz);
}
  • 我们直接看他唯一的实现类:RunAsManagerImpl
  • 可以看出来这里就是判断权限是否以RUN_AS开头,如果是的话新增以ROLE_开头的权限
java 复制代码
public Authentication buildRunAs(Authentication authentication, Object object,
      Collection<ConfigAttribute> attributes) {
   List<GrantedAuthority> newAuthorities = new ArrayList<>();
   for (ConfigAttribute attribute : attributes) {
      //判断权限是否以RUN_AS_开头
      if (this.supports(attribute)) {
         //创建新的权限名称,变成ROLE_开头的
         GrantedAuthority extraAuthority = new SimpleGrantedAuthority(
               getRolePrefix() + attribute.getAttribute());
         newAuthorities.add(extraAuthority);
      }
   }
   if (newAuthorities.size() == 0) {
      return null;
   }
   //新增替换后的权限
   newAuthorities.addAll(authentication.getAuthorities());
   //创建新的认证对象
   return new RunAsUserToken(this.key, authentication.getPrincipal(), authentication.getCredentials(),
         newAuthorities, authentication.getClass());
}

3. 总结

  • 最后讲下FilterSecurityInterceptor的完整逻辑:
    • 执行调用目标法前的权限判断
      • 通过安全元数据获得接口所需权限
      • 获得认对象:通过线程级别安全上下文策略 或者 局部认证管理器
      • 调用访问决策管理器确定是否放行该请求
        • 内部是借助访问决策投票器
      • 尝试以不同的认证方式执行(RunAsManager)
    • 执行后续的过滤器,但并不会返回
    • 执行调用目标法后的权限判断
相关推荐
Q_19284999065 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
陈无左耳、6 小时前
Spring Boot应用开发实战:从入门到精通
spring boot
烟波人长安吖~6 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
顽疲11 小时前
从零用java实现 小红书 springboot vue uniapp (6)用户登录鉴权及发布笔记
java·vue.js·spring boot·uni-app
编程洪同学11 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
GraduationDesign12 小时前
基于SpringBoot的蜗牛兼职网的设计与实现
java·spring boot·后端
customer0812 小时前
【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)
java·vue.js·spring boot·后端·kafka·开源·旅游
罗政16 小时前
PDF书籍《手写调用链监控APM系统-Java版》第10章 插件与链路的结合:SpringBoot环境插件获取应用名
java·spring boot·pdf
sin220116 小时前
springboot测试类里注入不成功且运行报错
spring boot·后端·sqlserver
kirito学长-Java17 小时前
springboot/ssm网上宠物店系统Java代码编写web宠物用品商城项目
java·spring boot·后端