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就沒問題?web

二)明確shiro-spring-boot-web-starter是否有對應的聲明

咱們在pom文件中聲明瞭shiro-spring-boot-web-starter。就從對應的jar包開始找起。
首先是META-INF中的spring.factories文件。咱們知道spring-boot-starter都是經過在該文件中聲明Configuraion來達到集成自身配置的目的。spring

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

上述聲明瞭兩個Configration:ShiroWebAutoConfiguration和ShiroWebFilterConfiguration。apache

  • 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,產生了影響?session

三)繼續查找其餘配置

觀察shiro-spring-boot-web-starter的配置文件,能夠看到它又引用了shiro-spring-boot-starter包。shrio-spring-boot-starter又是一個Spring boot starter包,一樣經過它的META-INF文件,能夠知道加入了哪些Configuration:app

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的條件,再試試。。。果真仍是不行。ide

四)DEBUG大法好

毫無辦法的辦法就是DEBUG大法。
首先從Configuration中生命的Bean是如何被容器加載的過程入手,找到了ConfigurationClassPostProcessor。一樣是一個PostProcessor,猜測應該是在configuration bean的後置處理中進行了@Bean方法的解析。
主要的處理過程在processConfigBeanDefinition這個方法中,對這個方法作個簡單的說明spring-boot

/**
     * 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正常的解析:ui

1

能夠看到authorizer確實已經被ShiroWebAutoConfiguration加載。this

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接口。真相大白。

相關文章
相關標籤/搜索