最近入手一个项目,用的是Spring Boot 1.5 + SiteMesh + Shiro,想赶时髦升级成Spring Boot 2,于是就掉坑里了。
正常情况从login网页登录后,页面转到index。但是升级完后却报了一个常见但又很难解决的问题:
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123)
at org.apache.shiro.subject.Subject$Builder.
at org.apache.shiro.SecurityUtils.getSubject(SecurityUtils.java:56)
一、原因分析
首先可以肯定的是Shiro的filter已经注册到filter chain里了,因为登录时输入用户名和密码有参与校验了,但是这个异常说明shiro filter没有起作用,这是为什么呢?
俗话说, 没有对比就没有伤害。但是这种棘手的问题却最好能通过对比来解决。
既然1.5版本的可以正常使用,那么就打开两个工程,把断点都打在SecurityUtils.getSubject(),看看都有什么内容。
1. 先看Spring Boot 1.5的,因为堆栈太长,只截了一部分图,但仍然很长。从图中可以看到ShiroFilter在起作用。
2. 再看看Spring Boot 2的,这个就短很多,没有看到Shiro Filter。
两张图对比下来,发现中间有一次forward。在1.5里,forward之后所有的filter又都重新执行了一遍,比如有两个SiteMeshFilter。而2里forward之后就只有一个WsFilter在执行。这中间有什么猫腻?
虽然在forward之后,Shiro filter消失了,但是在forward之前,filter chain里是有Shiro filter的,但是顺序排在SiteMeshFilter之后。
重点来了,咳咳。
SiteMeshFilter在处理时,调用了context.decorate(decoratorPath, content),这导致了ApplicationDispatcher.forward操作。
@Override
protected boolean postProcess(String contentType, CharBuffer buffer,
HttpServletRequest request, HttpServletResponse response,
ResponseMetaData metaData)
throws IOException, ServletException {
WebAppContext context = createContext(contentType, request, response, metaData);
Content content = contentProcessor.build(buffer, context);
if (content == null) {
return false;
}
String[] decoratorPaths = decoratorSelector.selectDecoratorPaths(content, context);
for (String decoratorPath : decoratorPaths) {
content = context.decorate(decoratorPath, content);
}
if (content == null) {
return false;
}
try {
content.getData().writeValueTo(response.getWriter());
} catch (IllegalStateException ise) { // If getOutputStream() has already been called
content.getData().writeValueTo(new PrintStream(response.getOutputStream()));
}
return true;
}
ApplicationDispatcher.forward操作里,又重新构建filter chain:
在这里面有一个matchDispatcher的函数,正是这个函数,导致Spring Boot 1.5和2的filter chain是不同的。1.5里所有的filter又都重新加载了,2里只有一个WsFilter被重新加载。而Forward之前的filter通通不见了。最惨的是Shiro Filter,刚好排在SiteMeshFilter之后,于是在Forward之前和之后都没有执行。
为什么forward之后filter都消失了呢?看看matchDispatcher的函数内部:
原来在Spring Boot 2里,大部分filter都不支持forward。
究其原因,是因为在filter registration的时候,filter的dispatcher type被赋予不同的值,代码位置在:
org.springframework.boot.web.servlet.AbstractFilterRegistrationBean.configure()
1. 这是Spring Boot 1.5的:
2. 这是Spring Boot 2的:
可以看到1.5里Forward,Include和Request dispatcher type都支持,而2里只支持Request dispatcher type。
二、解决方案
解决思路是让Shiro Filter能执行,把SecurityManager绑在ThreadContext里。
解决办法有两个,一是调整Filter的顺序,把Shiro Filter调到SiteMeshFilter的前面。二是让Shiro Filter也支持Forward。
方案一里,Shiro Filter是通过ShiroFilterFactoryBean来配置的,看不到调整Filter顺序的地方。反正我是没找到,诸位看官如果有办法的话,欢迎留言。
这里我用方案二,就是在ShiroConfig类里添加一个FilterRegistrationBean。
@Bean
public FilterRegistrationBean
FilterRegistrationBean
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
//filterRegistrationBean.setAsyncSupported(true);
EnumSet
DispatcherType.FORWARD);
filterRegistrationBean.setDispatcherTypes(types);
return filterRegistrationBean;
}
在Forward之后,Shiro Filter也可以被重新加载,于是问题得到解决。