Spring Boot 2 + SiteMesh + Shiro 入坑記
最近入手一個專案,用的是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.<init>(Subject.java:626)
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<DelegatingFilterProxy> delegatingFilterProxy(){
FilterRegistrationBean<DelegatingFilterProxy> filterRegistrationBean = new FilterRegistrationBean<>();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
//filterRegistrationBean.setAsyncSupported(true);
EnumSet<DispatcherType> types = EnumSet.of(DispatcherType.REQUEST,
DispatcherType.FORWARD);
filterRegistrationBean.setDispatcherTypes(types);
return filterRegistrationBean;
}
在Forward之後,Shiro Filter也可以被重新載入,於是問題得到解決。