Shiro踩坑记(一):关于shiro

Stella981
• 阅读 1026

一)问题描述:

我在一个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正常的解析:

Shiro踩坑记(一):关于shiro

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

2)解析没问题,那就看加载是否成功: 继续往下走,看reader.loadBeanDefinitions发生了什么:
Shiro踩坑记(一):关于shiro

找出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条件,并输出结果。

Shiro踩坑记(一):关于shiro

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

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

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
SpringBoot学习:整合shiro自动登录功能(rememberMe记住我功能)
首先在shiro配置类中注入rememberMe管理器!复制代码(https://oscimg.oschina.net/oscnet/675f5689159acfa2c39c91f4df40a00ce0f.gif)/cookie对象;rememberMeCookie()方法是设置Cookie的生成模
Stella981 Stella981
3年前
Shiro 核心功能案例讲解 基于SpringBoot 有源码
Shiro核心功能案例讲解基于SpringBoot有源码从实战中学习Shiro的用法。本章使用SpringBoot快速搭建项目。整合SiteMesh框架布局页面。整合Shiro框架实现用身份认证,授权,数据加密功能。通过本章内容,你将学会用户权限的分配规则,SpringBoot整合Sh
Easter79 Easter79
3年前
SpringBoot学习:整合shiro自动登录功能(rememberMe记住我功能)
首先在shiro配置类中注入rememberMe管理器!复制代码(https://oscimg.oschina.net/oscnet/675f5689159acfa2c39c91f4df40a00ce0f.gif)/cookie对象;rememberMeCookie()方法是设置Cookie的生成模
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这