springboot test shiro UnavailableSecurityManagerException
-
在做单元测试时遇到这个问题,场景是需要获得当前登录用户的id,正好可以使用shiro框架的SecurityUtils来获取,但是,测试一运行就报这个错误,然后就来分析一下
public abstract class SecurityUtils { ... public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException { SecurityManager securityManager = ThreadContext.getSecurityManager(); if (securityManager == null) { securityManager = SecurityUtils.securityManager; } if (securityManager == null) { String msg = "No SecurityManager accessible to the calling code, either bound to the " + ThreadContext.class.getName() + " or as a vm static singleton. This is an invalid application " + "configuration."; throw new UnavailableSecurityManagerException(msg); } return securityManager; } ... }
-
上面就是那段报错的代码,可以看到当
ThreadContext.getSecurityManager()
没有获取到securityManager时,并且SecurityUtils.securityManager
为空时(SecurityUtils.securityManager这个值没找到有哪个地方会对它进行赋值,所以一般情况,它是空的),就会报错。 -
主要原因还是在
ThreadContext
上,这个ThreadContext
是个什么东西呢?/** * A ThreadContext provides a means of binding and unbinding objects to the * current thread based on key/value pairs. * <p/> * <p>An internal {@link java.util.HashMap} is used to maintain the key/value pairs * for each thread.</p> * <p/> * <p>If the desired behavior is to ensure that bound data is not shared across * threads in a pooled or reusable threaded environment, the application (or more likely a framework) must * bind and remove any necessary values at the beginning and end of stack * execution, respectively (i.e. individually explicitly or all via the <tt>clear</tt> method).</p> * * @see #remove() * @since 0.1 */ public abstract class ThreadContext { private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>(); }
- 可以知道它是为每个线程维护键值对数据的工具,翻译过来叫线程上下文,名字也贴合它的功能,实现是利用了ThreadLocal,这个类之前也分析过,在此不作赘述。
-
了解了
ThreadContext
,就来看看什么时候putSecurityManager
实例的。-
一层层往上找,发现
-
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal
- org.apache.shiro.subject.support.DelegatingSubject#execute
- org.apache.shiro.subject.support.SubjectCallable#call
- org.apache.shiro.subject.support.SubjectThreadState#bind
- org.apache.shiro.util.ThreadContext#bind
- org.apache.shiro.subject.support.SubjectThreadState#bind
- org.apache.shiro.subject.support.SubjectCallable#call
- org.apache.shiro.subject.support.DelegatingSubject#execute
-
发现这个bind动作是shiro的过滤器触发的;而且每个请求触发一次,这个可以从
org.apache.shiro.web.servlet.OncePerRequestFilter
看出。 -
所以,到这里可以得出一个结论,每个请求bind数据到
ThreadContext
,这样,在这个线程执行的任何地方,都可以获取到bind的数据,但是,为什么测试不行呢?猜想一下,那就是这个线程结束了,自然就获取不到bind的数据了。 -
接着来调试下单元测试,看了看调用栈发现不是之前那个bind了数据的线程,从调用栈就可以看出来,根本就没有bind的踪迹,说明猜想对了,与此同时,去shiro的官网找了下,发现它提供了shiro和单元测试使用的demo,看来它这个问题一直都有的。
-
-
-
解决办法有很多,只要设置一个
SecurityManager
实例就可以了,比如注入一个实例到SecurityUtils.securityManager
,测试就可以运行了。 -
总结
- 这次问题查找,熟悉了调用栈的使用,然后对一个请求的过滤器处理过程,以及责任链模式有了新的印象
- 还需要再深入的话,就需要看看junit的实现原理了,还有spring集成测试是怎么实现的