• Shiro踩坑记(一):关于shiro-spring-boot-web-starter自动注解无法注入authorizer的问题


    一)问题描述:

    我在一个Spring的项目中使用shiro搭建权限控制框架。主要通过shiro-spring-boot-web-starter包快速集成Shiro。但是项目无法启动,报没有authorizer的bean的错误:  
    
    ```
    No bean named 'authorizer' available
    ```  
    
    我只好又在自己的Configuration中又配置了Authorizer,才能正常启动。  
    
    @Configuration
    public class ShiroConfig {
    
    @Bean
    public Authorizer authorizer(){
        return new ModularRealmAuthorizer();
    }
    }
    

    但是奇怪的明明athorizer是SecurityManager中一个重要的组件,为什么没有在shiro starter的Configuration中被声明为Bean?同样的,Authenticator就没问题?

    二)明确shiro-spring-boot-web-starter是否有对应的声明

    我们在pom文件中声明了shiro-spring-boot-web-starter。就从对应的jar包开始找起。
    首先是META-INF中的spring.factories文件。我们知道spring-boot-starter都是通过在该文件中声明Configuraion来达到集成自身配置的目的。

    org.springframework.boot.autoconfigure.EnableAutoConfiguration = 
    org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,
    org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration
    
    

    上述声明了两个Configration:ShiroWebAutoConfiguration和ShiroWebFilterConfiguration。

    • ShiroWebFilterConfiguration
      先从简单的配置说起,ShiroWebFilterConfiguration是以添加Filter的方式来达到authentication的目的。这个和我们的问题无关,简单带过。
    • ShiroWebAutoConfiguration
    @Configuration
    @AutoConfigureBefore(ShiroAutoConfiguration.class)
    @ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
    public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected AuthenticationStrategy authenticationStrategy() {
            return super.authenticationStrategy();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected Authenticator authenticator() {
            return super.authenticator();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected Authorizer authorizer() {
            return super.authorizer();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SubjectDAO subjectDAO() {
            return super.subjectDAO();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionStorageEvaluator sessionStorageEvaluator() {
            return super.sessionStorageEvaluator();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SubjectFactory subjectFactory() {
            return super.subjectFactory();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionFactory sessionFactory() {
            return super.sessionFactory();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionDAO sessionDAO() {
            return super.sessionDAO();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionManager sessionManager() {
            return super.sessionManager();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionsSecurityManager securityManager(List<Realm> realms) {
            return createSecurityManager();
        }
    
        @Bean
        @ConditionalOnMissingBean(name = "sessionCookieTemplate")
        @Override
        protected Cookie sessionCookieTemplate() {
            return super.sessionCookieTemplate();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected RememberMeManager rememberMeManager() {
            return super.rememberMeManager();
        }
    
        @Bean
        @ConditionalOnMissingBean(name = "rememberMeCookieTemplate")
        @Override
        protected Cookie rememberMeCookieTemplate() {
            return super.rememberMeCookieTemplate();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
            return super.shiroFilterChainDefinition();
        }
    }
    
    

    这个配置类将Shiro需要的各组件都声明成了bean,交给容器管理。具体的创建过程都在父类AbstractShiroWebConfiguration。可以看到确实是有声明authorizer。但是为什么会找不到呢?是不是其他的配置文件声明了类似的bean,产生了影响?

    三)继续查找其他配置

    观察shiro-spring-boot-web-starter的配置文件,可以看到它又引用了shiro-spring-boot-starter包。shrio-spring-boot-starter又是一个Spring boot starter包,同样通过它的META-INF文件,可以知道加入了哪些Configuration:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration = 
      org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration,
      org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration,
      org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration
    
    org.springframework.boot.diagnostics.FailureAnalyzer = 
      org.apache.shiro.spring.boot.autoconfigure.ShiroNoRealmConfiguredFailureAnalyzer
    

    最后一个文件是判断项目中不存在Realm时,抛出异常。前面是我们需要关注的配置文件。

    • ShiroAnnotationProcessorAutoConfiguration
      该配置主要是通过AOP的方式实现authorization的功能。
    • ShiroBeanAutoConfiguraion
      主要是通过添加BeanPostProcessor,在Shiro相关的Bean初始化时,做一些额外的操作。
    • ShiroAutoConfiguration
    @Configuration
    @SuppressWarnings("SpringFacetCodeInspection")
    @ConditionalOnProperty(name = "shiro.enabled", matchIfMissing = true)
    public class ShiroAutoConfiguration extends AbstractShiroConfiguration {
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected AuthenticationStrategy authenticationStrategy() {
            return super.authenticationStrategy();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected Authenticator authenticator() {
            return super.authenticator();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected Authorizer authorizer() {
            return super.authorizer();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SubjectDAO subjectDAO() {
            return super.subjectDAO();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionStorageEvaluator sessionStorageEvaluator() {
            return super.sessionStorageEvaluator();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SubjectFactory subjectFactory() {
            return super.subjectFactory();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionFactory sessionFactory() {
            return super.sessionFactory();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionDAO sessionDAO() {
            return super.sessionDAO();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionManager sessionManager() {
            return super.sessionManager();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        protected SessionsSecurityManager securityManager(List<Realm> realms) {
            return super.securityManager(realms);
        }
    
        @Bean
        @ConditionalOnResource(resources = "classpath:shiro.ini")
        protected Realm iniClasspathRealm() {
            return iniRealmFromLocation("classpath:shiro.ini");
        }
    
        @Bean
        @ConditionalOnResource(resources = "classpath:META-INF/shiro.ini")
        protected Realm iniMetaInfClasspathRealm() {
            return iniRealmFromLocation("classpath:META-INF/shiro.ini");
        }
    
        @Bean
        @ConditionalOnMissingBean(Realm.class)
        protected Realm missingRealm() {
            throw new NoRealmBeanConfiguredException();
        }
    
    
    }
    

    大致内容其实和ShiroWebAutoConfiguration很类似,只是ShiroWebAutoConfiguration将一些组件替换成了WEB环境相关的组件。但是ShiroWebAutoConfiguration声明了它的配置要在ShiroAutoConfiguration之前,而且根据ConditionalOnMissingBean的条件,得出Bean的配置应该是以ShiroWebAutoConfiguration中声明的为准。但是死马当活马医,配置文件中添加shiro.enabled为false的条件,再试试。。。果然还是不行。

    四)DEBUG大法好

    毫无办法的办法就是DEBUG大法。
    首先从Configuration中生命的Bean是如何被容器加载的过程入手,找到了ConfigurationClassPostProcessor。同样是一个PostProcessor,猜想应该是在configuration bean的后置处理中进行了@Bean方法的解析。
    主要的处理过程在processConfigBeanDefinition这个方法中,对这个方法做个简单的说明

    /**
    	 * Build and validate a configuration model based on the registry of
    	 * {@link Configuration} classes.
    	 */
    	public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    		List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
    		//获取registry中的bean definition
    		String[] candidateNames = registry.getBeanDefinitionNames();
    
    		for (String beanName : candidateNames) {
    			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
    			//bean definition 有configuration的属性,说明已经被解析处理过
    			if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
    					ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
    				if (logger.isDebugEnabled()) {
    					logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
    				}
    			}
    			//判断是否是configuration的bean,是则加入候选
    			else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
    				configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
    			}
    		}
    
    		// 如果没有发现候选者,则返回
    		if (configCandidates.isEmpty()) {
    			return;
    		}
    
    		// 排序
    		configCandidates.sort((bd1, bd2) -> {
    			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
    			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
    			return Integer.compare(i1, i2);
    		});
    
    		// Detect any custom bean name generation strategy supplied through the enclosing application context
    		SingletonBeanRegistry sbr = null;
    		if (registry instanceof SingletonBeanRegistry) {
    			sbr = (SingletonBeanRegistry) registry;
    			if (!this.localBeanNameGeneratorSet) {
    				BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
    				if (generator != null) {
    					this.componentScanBeanNameGenerator = generator;
    					this.importBeanNameGenerator = generator;
    				}
    			}
    		}
    
    		if (this.environment == null) {
    			this.environment = new StandardEnvironment();
    		}
    
    		// 开始解析configuration 的bean definition
    		ConfigurationClassParser parser = new ConfigurationClassParser(
    				this.metadataReaderFactory, this.problemReporter, this.environment,
    				this.resourceLoader, this.componentScanBeanNameGenerator, registry);
    
    		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
    		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
    
    		// 如果候选者不为空,则继续解析
    		do {
    			// 解析过程
    			parser.parse(candidates);
    			// 校验
    			parser.validate();
    
    			// 获取新解析的config class	
    			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
    			// 移除掉已经解析过的部分
    			configClasses.removeAll(alreadyParsed);
    
    			// 创建reader,添加bean definition
    			if (this.reader == null) {
    				this.reader = new ConfigurationClassBeanDefinitionReader(
    						registry, this.sourceExtractor, this.resourceLoader, this.environment,
    						this.importBeanNameGenerator, parser.getImportRegistry());
    			}
    			this.reader.loadBeanDefinitions(configClasses);
    			alreadyParsed.addAll(configClasses);
    
    			candidates.clear();
    			//如果bean definition数量 大于 候选者的数量,说明有新的bean加入
    			if (registry.getBeanDefinitionCount() > candidateNames.length) {
    				String[] newCandidateNames = registry.getBeanDefinitionNames();
    				Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
    
    				Set<String> alreadyParsedClasses = new HashSet<>();
    
    				for (ConfigurationClass configurationClass : alreadyParsed) {
    					alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
    				}
    
    				for (String candidateName : newCandidateNames) {
    					//不在旧的candidate中,说明是新加入的
    					if (!oldCandidateNames.contains(candidateName)) {
    
    						BeanDefinition bd = registry.getBeanDefinition(candidateName);
    						//未被解析的config class,添加到candidates中,等下一轮解析
    						if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
    								!alreadyParsedClasses.contains(bd.getBeanClassName())) {
    							candidates.add(new BeanDefinitionHolder(bd, candidateName));
    						}
    					}
    				}
    				//更新候选者
    				candidateNames = newCandidateNames;
    			}
    		}
    		while (!candidates.isEmpty());
    
    		// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
    		if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
    			sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
    		}
    
    		if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
    			// Clear cache in externally provided MetadataReaderFactory; this is a no-op
    			// for a shared cache since it'll be cleared by the ApplicationContext.
    			((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
    		}
    	}
    

    1)parser.parse后设置断点,看ConfigurationClassParser是否能将ShiroWebAutoConfiguration中的@Bean正常的解析:

    1

    可以看到authorizer确实已经被ShiroWebAutoConfiguration加载。

    2)解析没问题,那就看加载是否成功:
    继续往下走,看reader.loadBeanDefinitions发生了什么:
    2

    找出ShiroWebAutoConfiguration对应的ConfigurationClass,看到SkippedBeanMethods中有authorizer!!!也就是说虽然解析出了authorizer,但是在加载的时候却被选择跳过了。。。
    3)问题就变得比较清晰了,找出为什么被跳过的原因。
    顺着代码找到ConfigurationClassBeanDefinitionReader的loadBeanDefinitionsForConfigurationClass的方法,负责处理的BeanMethond的过程是在loadBeanDefitionsForBeanMethod中。
    确实在方法开始前,有一个判断是否需要跳过的条件:

    if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
    			configClass.skippedBeanMethods.add(methodName);
    			return;
    		}
    

    shouldSkip这个方法是根据@Bean上的@Conditional注解,来判断是否需要加载该Bean。回忆上文我们的ShiroWebAutoConfiguration中,确实在authorizer的方法上有@ConditionalOnMissingBean的注解。也就是说应该是哪里声明authorizer的Bean,导致配置中的Bean没有被加载。
    4)OnBeanCondition.getMatchOutcome():处理@Bean的@Condtional条件,并输出结果。

    3

    最后发现被跳过的原因竟然是:

    found beans of type 'org.apache.shiro.authz.Authorizer' authorizer, thirdPartyRealm, userRealm
    

    我自定义的Realm竟然和authorizer冲突了。Spring认为已经有authorizer的bean,而不再加载配置中的authorizer。
    5)为什么Realm和authorizer冲突?原来在获取相匹配的Bean时候还是通过容器本身(BeanFactory)的getNamesForType方法:

    	Set<String> getNamesForType(Class<?> type) {
    		updateTypesIfNecessary();
    		//便利容器中所有的bean类型,将类型匹配的Type全部返回。注意这里还用了isAssiginableFrom,因此这里的查询类型的子类也会满足
    		return this.beanTypes.entrySet().stream()
    				.filter((entry) -> entry.getValue() != null
    						&& type.isAssignableFrom(entry.getValue()))
    				.map(Map.Entry::getKey)
    				.collect(Collectors.toCollection(LinkedHashSet::new));
    	}
    

    反观我们的Realm对象:AuthorizingRealm实现了Authorizer接口。真相大白。

  • 相关阅读:
    react dva 的 connect 与 @connect
    es6 解构赋值 新认知/新习惯
    从一到面试题了解js异步机制:setTimeout 和 Pronmise
    React.Fragment 的作用:代替div作为外层
    解决dva dispatch yield生成器函数中异常中断,无法继续调用的问题
    vue v-model 与 组件化的表单组件如何沟通
    react 事件绑定的2种常用方式
    React dva 的使用
    gulp#4.0 Did you forget to signal async completion?
    gulp#4.0
  • 原文地址:https://www.cnblogs.com/insaneXs/p/11028286.html
Copyright © 2020-2023  润新知