[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实例化初始化前后的回调
    • 动态代理
相关推荐
程序猿麦小七16 分钟前
基于springboot的景区网页设计与实现
java·spring boot·后端·旅游·景区
蓝田~24 分钟前
SpringBoot-自定义注解,拦截器
java·spring boot·后端
theLuckyLong25 分钟前
SpringBoot后端解决跨域问题
spring boot·后端·python
A陈雷25 分钟前
springboot整合elasticsearch,并使用docker desktop运行elasticsearch镜像容器遇到的问题。
spring boot·elasticsearch·docker
.生产的驴26 分钟前
SpringCloud Gateway网关路由配置 接口统一 登录验证 权限校验 路由属性
java·spring boot·后端·spring·spring cloud·gateway·rabbitmq
小扳30 分钟前
Docker 篇-Docker 详细安装、了解和使用 Docker 核心功能(数据卷、自定义镜像 Dockerfile、网络)
运维·spring boot·后端·mysql·spring cloud·docker·容器
v'sir40 分钟前
POI word转pdf乱码问题处理
java·spring boot·后端·pdf·word
李少兄44 分钟前
解决Spring Boot整合Redis时的连接问题
spring boot·redis·后端
冰逸.itbignyi1 小时前
SpringBoot之AOP 的使用
java·spring boot
码上一元6 小时前
SpringBoot自动装配原理解析
java·spring boot·后端