1. 程式人生 > >Spring Boot 2 + SiteMesh + Shiro 入坑記

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也可以被重新載入,於是問題得到解決。