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正常的解析:
可以看到authorizer确实已经被ShiroWebAutoConfiguration加载。
2)解析没问题,那就看加载是否成功:
继续往下走,看reader.loadBeanDefinitions发生了什么:
找出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条件,并输出结果。
最后发现被跳过的原因竟然是:
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接口。真相大白。
Shiro踩坑记(一):关于shiro-spring-boot-web-starter自动注解无法注入authorizer的问题的更多相关文章
- Shiro踩坑记(二):使用RequiresXXX的注解后,访问对应请求返回404
问题描述: 我在项目中的某个Controller上添加了@RequirePermissions注解,希望在执行该请求前,可以先进行权限验证.但是当我请求该Controller时,返回的确是404错误. ...
- spring-boot-starter-amqp踩坑记
踩坑记录 近日在用spring boot架构一个微服务框架,服务发现与治理.发布REST接口各种轻松惬意.但是服务当设计MQ入口时,就发现遇到无数地雷,现在整理成下文,供各路大侠围观与嘲笑. 版本 当 ...
- 记一次 Spring 事务配置踩坑记
记一次 Spring 事务配置踩坑记 问题描述:(SpringBoot + MyBatisPlus) 业务逻辑伪代码如下.理论上,插入数据 t1 后,xxService.getXxx() 方法的查询条 ...
- Spring @Transactional踩坑记
@Transactional踩坑记 总述 Spring在1.2引入@Transactional注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional注解,实现事务 ...
- Spark踩坑记——Spark Streaming+Kafka
[TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...
- Spark踩坑记——数据库(Hbase+Mysql)
[TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...
- 【踩坑记】从HybridApp到ReactNative
前言 随着移动互联网的兴起,Webapp开始大行其道.大概在15年下半年的时候我接触到了HybridApp.因为当时还没毕业嘛,所以并不清楚自己未来的方向,所以就投入了HybridApp的怀抱. Hy ...
- Spark踩坑记——共享变量
[TOC] 前言 Spark踩坑记--初试 Spark踩坑记--数据库(Hbase+Mysql) Spark踩坑记--Spark Streaming+kafka应用及调优 在前面总结的几篇spark踩 ...
- Spark踩坑记——从RDD看集群调度
[TOC] 前言 在Spark的使用中,性能的调优配置过程中,查阅了很多资料,之前自己总结过两篇小博文Spark踩坑记--初试和Spark踩坑记--数据库(Hbase+Mysql),第一篇概况的归纳了 ...
随机推荐
- Shell:Day05.笔记
交互输入与for语句 1.交互输入 read Python中用input()函数,进行输入: read命令同时可以定义多个变量值:而输入的内容默认以空格为分隔符,将值输入到对应的变量中: 如果默认 ...
- 听说这个 IP 和子网掩码异常难算
IP地址格式 每个Internet主机或路由器都有IP地址.所有的IP地址包括网络号和主机号(就像是手机号,前几位是区号,后几位是序列号). 说明如下 A类地址用于主机数目非常多的网络.A类地址允许有 ...
- 8.1 api概述及使用
api文件:JDK_API_1.6.CHM 1:打开帮助文档2:点击显示,找到索引,看到输入框3:你要学习什么内容,你就在框框里面输入什么内容 举例:Random4:看包 java.lang包下的类在 ...
- leetcode c++做题思路和题解(3)——栈的例题和总结
栈的例题和总结 0. 目录 有效的括号 栈实现队列(这个参见队列) 1. 有效的括号 static int top = 0; static char* buf = NULL; void stack(i ...
- 在linux中使用mailx发送邮件
[root@ml ~]# yum -y install mailx #安装 [root@ml ~]# vim /etc/mail.rc 在最后一行添加(我这里使用的是qq邮箱): @qq.com ...
- AJ学IOS 之CoreLocation反地理编码小Demo输入经纬度得到城市
AJ分享,必须精品 一:效果 输入经纬度,可以得到相应的地名 二:思路 跟地里编码差不多 1.获取用户输入的经纬度 2.根据用户输入的经纬度创建CLLocation对象 3.根据CLLocation对 ...
- adb命令查看手机应用内存使用情况
adb shell回车 一.procrank VSS >= RSS >= PSS >= USSVSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)是单个 ...
- 6. 浅谈super
this和super: super( ) EX6类的继承, 在react中官方固定应用 在java面向对象思想中这样定义: this表示当前对象,this()为当前对象的其他构造函数 super表示父 ...
- CTFHub web技能树之RCE初步 命令注入+过滤cat
在一个大佬的突然丢了个题过来,于是去玩了玩rce的两道题 大佬的博客跳转链接在此->>>大佬召唤机 叫 命令注入 一上来就是源码出现,上面有个ping的地方 <?php $re ...
- 使用hexo和coding建立静态博客站点
背景 由于工作性质的原因,做技术的总想记录和分享一下自己的学习和成长历程,向这世界证明我来过.写文章,发博客,一开始使用51cto,广告太多,看起来让人很痛苦:接着试用了博客园,广告少一些,但感觉还是 ...