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

1.GlobalMethodSecurityConfiguration

  • GlobalMethodSecurityConfiguration的核心是为了注册MethodSecurityInterceptor
  • MethodSecurityInterceptor :是最终注册到代理类(DynamicAdvisedInterceptor )中的拦截器,会在被代理方法执行前后校验权限,支持的注解如下:
    • @PreAuthorize:在方法执行前调用,判断是否有指定权限
    • @PostAuthorize:在方法执行后调用,可以用来判断权限,或者对返回值判断
    • @PreFilter:在方法执行前被调用:对于入参中的集合进行过滤,不符合的将会被删除
    • @PostFilter:和PreFilter 很像,不过是在方法执行后,对于返回值进行过滤
  • 下图就是此配置类注册的核心类
  • 接下来先介绍此配置类的重要方法

1.1 afterInvocationManager()

  • afterInvocationManager():创建执行后管理器(AfterInvocationManager)
java 复制代码
protected AfterInvocationManager afterInvocationManager() {
   if (prePostEnabled()) {
      AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
      ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
            getExpressionHandler());
      PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice);
      List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<>();
      afterInvocationProviders.add(postInvocationAdviceProvider);
      invocationProviderManager.setProviders(afterInvocationProviders);
      return invocationProviderManager;
   }
   return null;
}
  • AfterInvocationManager:当目标方法上带有@PostAuthorize和@PostFilter并且目标方法执行完毕后,开始校验权限和过滤返回值
java 复制代码
public interface AfterInvocationManager {

   /**
    * 根据处理方法执行后权限表达式和返回值进行操作
    */
   Object decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes,
         Object returnedObject) throws AccessDeniedException;

   /**
    * 此 {@link AfterInvocationManager} 是否支持解析 {@link ConfigAttribute}
    */
   boolean supports(ConfigAttribute attribute);

   boolean supports(Class<?> clazz);

}

1.2 accessDecisionManager()

  • accessDecisionManager():创建访问决策管理器
java 复制代码
protected AccessDecisionManager accessDecisionManager() {
   List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
   // 开启了prePost的权限注解,创建对应的访问决策投票器
   if (prePostEnabled()) {
      ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
      expressionAdvice.setExpressionHandler(getExpressionHandler());
      decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
   }

   // 开启了jsr250的权限注解,创建对应的访问决策投票器
   if (jsr250Enabled()) {
      decisionVoters.add(new Jsr250Voter());
   }
   RoleVoter roleVoter = new RoleVoter();
   // 获得角色前缀
   GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
   if (grantedAuthorityDefaults != null) {
      roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
   }

   // 有任何一个角色匹配就投同意票
   decisionVoters.add(roleVoter);

   // 根据认证方式投票
   decisionVoters.add(new AuthenticatedVoter());
   return new AffirmativeBased(decisionVoters);
}

1.3 authenticationManager()

  • authenticationManager():创建认证管理器
java 复制代码
protected AuthenticationManager authenticationManager() throws Exception {
   if (this.authenticationManager == null) {
      DefaultAuthenticationEventPublisher eventPublisher = this.objectPostProcessor
            .postProcess(new DefaultAuthenticationEventPublisher());
      this.auth = new AuthenticationManagerBuilder(this.objectPostProcessor);
      this.auth.authenticationEventPublisher(eventPublisher);
      configure(this.auth);

      // 是使用容器中的还是用构建起创建出来的
      this.authenticationManager = (this.disableAuthenticationRegistry)
            ? getAuthenticationConfiguration().getAuthenticationManager() : this.auth.build();
   }
   return this.authenticationManager;
}

1.4 methodSecurityMetadataSource()

  • methodSecurityMetadataSource():注册安全元数据源
java 复制代码
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
   List<MethodSecurityMetadataSource> sources = new ArrayList<>();
   ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
         getExpressionHandler());

   // 自定义安全元数据源
   MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
   if (customMethodSecurityMetadataSource != null) {
      sources.add(customMethodSecurityMetadataSource);
   }
   boolean hasCustom = customMethodSecurityMetadataSource != null;

   // 是否开启了下面三种类型的注解
   boolean isPrePostEnabled = prePostEnabled();
   boolean isSecuredEnabled = securedEnabled();
   boolean isJsr250Enabled = jsr250Enabled();
   Assert.state(isPrePostEnabled || isSecuredEnabled || isJsr250Enabled || hasCustom,
         "In the composition of all global method configuration, "
               + "no annotation support was actually activated");

   // 尝试添加下面三种安全元数据源
   if (isPrePostEnabled) {
      sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
   }
   if (isSecuredEnabled) {
      sources.add(new SecuredAnnotationSecurityMetadataSource());
   }
   if (isJsr250Enabled) {
      GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
      Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context
            .getBean(Jsr250MethodSecurityMetadataSource.class);
      // 设置角色前缀
      if (grantedAuthorityDefaults != null) {
         jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
      }
      sources.add(jsr250MethodSecurityMetadataSource);
   }
   return new DelegatingMethodSecurityMetadataSource(sources);
}

1.5 preInvocationAuthorizationAdvice()

  • preInvocationAuthorizationAdvice():注册PreInvocationAuthorizationAdvice
java 复制代码
@Bean
public PreInvocationAuthorizationAdvice preInvocationAuthorizationAdvice() {
   ExpressionBasedPreInvocationAdvice preInvocationAdvice = new ExpressionBasedPreInvocationAdvice();
   preInvocationAdvice.setExpressionHandler(getExpressionHandler());
   return preInvocationAdvice;
}
  • PreInvocationAuthorizationAdvice:在调用处理方法之前调用,进行参数过滤和权限判断,是在访问决策投票器(PreInvocationAuthorizationAdviceVoter)中被执行的,通过此Advice决定此投票器的最终结果

1.7 methodSecurityInterceptor()

  • methodSecurityInterceptor():注册 MethodSecurityInterceptor
java 复制代码
@Bean
public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource) {
   this.methodSecurityInterceptor = isAspectJ() ? new AspectJMethodSecurityInterceptor()
         : new MethodSecurityInterceptor();
   //设置访问决策管理器
   this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
   //设置执行后管理器
   this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
   //设置安全元数据源
   this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
   RunAsManager runAsManager = runAsManager();
   if (runAsManager != null) {
      this.methodSecurityInterceptor.setRunAsManager(runAsManager);
   }
   return this.methodSecurityInterceptor;
}

2. 基于权限注解如何创建代理对象

  • 虽然SpringSecurity通过AutoProxyRegistrar往容器注册了一个InfrastructureAdvisorAutoProxyCreator对应的RootBeanDefinition,但实际上注册到容器中的Bean却是AnnotationAwareAspectJAutoProxyCreator
    • 这是因为我们注册Bean的名称是下面这个,但是SpringBoot在运行过程中会以此名称注册多次
    • 所以当名称重复时就会以Class在下面这个List的位置来决定优先级,值越大优先级越高
  • 然后就到Bean的初始化阶段,我们看AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization(...)方法:根据Advice和Advisor确定是否需要创建代理对象
java 复制代码
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
   if (bean != null) {
      Object cacheKey = getCacheKey(bean.getClass(), beanName);
      if (this.earlyProxyReferences.remove(cacheKey) != bean) {
         return wrapIfNecessary(bean, beanName, cacheKey);
      }
   }
   return bean;
}
    • 由于内部的调用链非常复杂又涉及到了Spring的知识点,我们这里直接单刀直入,进入GlobalMethodSecuritySelector中注册的PrePostAnnotationSecurityMetadataSource
  • 再看PrePostAnnotationSecurityMetadataSource 的getAttributes(...)方法
    • 很明显只有带有特定的注解返回集合才不为空,才会为Bean创建代理对象
java 复制代码
public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
   if (method.getDeclaringClass() == Object.class) {
      return Collections.emptyList();
   }
   // 在方法和方法的声明类上查找下面四种指定注解
   PreFilter preFilter = findAnnotation(method, targetClass, PreFilter.class);
   PreAuthorize preAuthorize = findAnnotation(method, targetClass, PreAuthorize.class);
   PostFilter postFilter = findAnnotation(method, targetClass, PostFilter.class);
   // TODO: 由于@PostAuthorize可以针对于返回值,所以可以在这里检查空返回值,然后抛出异常
   PostAuthorize postAuthorize = findAnnotation(method, targetClass, PostAuthorize.class);

   // 方法上没有这四种注解,直接返回空集合
   if (preFilter == null && preAuthorize == null && postFilter == null && postAuthorize == null) {
      // There is no meta-data so return
      return Collections.emptyList();
   }

   // 获得这些注解的表达式
   String preFilterAttribute = (preFilter != null) ? preFilter.value() : null;
   String filterObject = (preFilter != null) ? preFilter.filterTarget() : null;
   String preAuthorizeAttribute = (preAuthorize != null) ? preAuthorize.value() : null;
   String postFilterAttribute = (postFilter != null) ? postFilter.value() : null;
   String postAuthorizeAttribute = (postAuthorize != null) ? postAuthorize.value() : null;


   ArrayList<ConfigAttribute> attrs = new ArrayList<>(2);
   // 创建方法执行前需要判断的表达式
   PreInvocationAttribute pre = this.attributeFactory.createPreInvocationAttribute(preFilterAttribute,
         filterObject, preAuthorizeAttribute);
   if (pre != null) {
      attrs.add(pre);
   }

   // 创建方法执行后需要判断的表达式
   PostInvocationAttribute post = this.attributeFactory.createPostInvocationAttribute(postFilterAttribute,
         postAuthorizeAttribute);
   if (post != null) {
      attrs.add(post);
   }
   attrs.trimToSize();
   return attrs;
}
  • 而创建的代理对象以及注册到容器中的正是:DynamicAdvisedInterceptor ,就像下面这样
    • 每当执行代理对象的时候就会执行其intercept(...)方法,而在intercept(...)方法中有一行代码
    • 这里就会根据入参获得执行目标方法前需要执行的拦截器,这里就会有我们注册的MethodSecurityInterceptor
    • 也不仅仅只这一个,像CacheInterceptor,TransactionInterceptor都是在这里工作的

3. MethodSecurityInterceptor工作流程

  • 直接看invoke(...)方法
java 复制代码
public Object invoke(MethodInvocation mi) throws Throwable {
   // 调用前的权限判断
   InterceptorStatusToken token = super.beforeInvocation(mi);
   Object result;
   try {
      // 执行真正的处理方法
      result = mi.proceed();
   }
   finally {
      // 是否需要将认证对象恢复到 判断权限之前
      super.finallyInvocation(token);
   }
   return super.afterInvocation(token, result);
}
  • invoke(...)方法的核心在于beforeInvocation(...)和afterInvocation(...)方法,这两个方法分别是在目标方法执行前和执行后校验权限的,由于beforeInvocation(...)前面讲了,所以只讲afterInvocation(...)
  • afterInvocation(...):
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;
}

4. 总结

  • MethodSecurityInterceptor 其本身原理就是:
    • 第一步利用SecurityMetadataSource拿到访问接口的权限属性,
    • 第二步借助AuthenticationManager获取认证对象
    • 第三步使用AccessDecisionManager以及里面的AccessDecisionVoter的进行投票确定,确定是否通过
  • 这里其实和FilterSecurtiyFilter很像的
  • 但是在上面的步骤之前要有一定的前置知识点要解锁
    • Spring中是如何借助BeanPostProcessor机制完成对于Bean实例化初始化前后的回调
    • 动态代理
相关推荐
猿来入此小猿3 分钟前
基于SpringBoot在线音乐系统平台功能实现十二
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
愤怒的代码16 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
栗豆包32 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
万亿少女的梦1681 小时前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端
开心工作室_kaic2 小时前
springboot485基于springboot的宠物健康顾问系统(论文+源码)_kaic
spring boot·后端·宠物
爱码少年6 小时前
springboot中责任链模式之简单应用
spring boot·责任链模式
苹果酱05677 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
武昌库里写JAVA7 小时前
【MySQL】7.0 入门学习(七)——MySQL基本指令:帮助、清除输入、查询等
spring boot·spring·毕业设计·layui·课程设计
黑胡子大叔的小屋13 小时前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
计算机毕设孵化场13 小时前
计算机毕设-基于springboot的校园社交平台的设计与实现(附源码+lw+ppt+开题报告)
spring boot·课程设计·计算机毕设论文·计算机毕设ppt·计算机毕业设计选题推荐·计算机选题推荐·校园社交平台