1. 程式人生 > >springmvc整合shiro後,session、request姓汪還是姓蔣?

springmvc整合shiro後,session、request姓汪還是姓蔣?

1. 疑問

我們在專案中使用了spring mvc作為MVC框架,shiro作為許可權控制框架,在使用過程中慢慢地產生了下面幾個疑惑,本篇文章將會帶著疑問慢慢地解析shiro原始碼,從而解開心裡面的那點小糾糾。

(1)在spring controller中,request有何不同呢

於是,在controller中列印了request的類物件,發現request物件是org.apache.shiro.web.servlet.ShiroHttpServletRequest ,很明顯,此時的 request 已經被shiro包裝過了。

(2)眾所周知,spring mvc整合shiro後,可以通過兩種方式獲取到session:

通過Spring mvc中controller的request獲取session

 Session session = request.getSession();

通過shiro獲取session

 Subject currentUser = SecurityUtils.getSubject();
 Session session = currentUser.getSession();

那麼,問題來了,兩種方式獲取的session是否相同呢

這裡需要看一下專案中的shiro的securityManager配置,因為配置影響了shiro session的來源。這裡沒有配置session管理器。

    <beanid="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    
        <!--<property name="sessionManager" ref="sessionManager"/>-->

        <propertyname="realm"ref="shiroRealm"/>
    </bean>

在controller中再次列印了session,發現前者的session型別是 org.apache.catalina.session.StandardSessionFacade ,後者的session型別是org.apache.shiro.subject.support.DelegatingSubject$StoppingAwareProxiedSession。

很明顯,前者的session是屬於httpServletRequest中的HttpSession,那麼後者呢?仔細看StoppingAwareProxiedSession,它是屬於shiro自定義的session的子類。通過這個代理物件的原始碼,我們發現所有與session相關的方法都是通過它內部委託類delegate進行的,通過斷點,可以看到delegate的型別其實也是 org.apache.catalina.session.StandardSessionFacade ,也就是說,兩者在操作session時,都是用同一個型別的session。那麼它什麼時候包裝了httpSession呢?

輸入圖片說明

2. 一起一層一層剝開它的芯

2.1 怎麼獲取過濾器filter

spring mvc 整合shiro,需要在web.xml中配置該filter

    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <!-- 設定spring容器filter的bean id,如果不設定則找與filter-name一致的bean-->
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>shiroFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>

DelegatingFilterProxy 是一個過濾器,準確來說是目的過濾器的代理,由它在doFilter方法中,獲取spring 容器中的過濾器,並呼叫目標過濾器的doFilter方法,這樣的好處是,原來過濾器的配置放在web.xml中,現在可以把filter的配置放在spring中,並由spring管理它的生命週期。另外,DelegatingFilterProxy中的targetBeanName指定需要從spring容器中獲取的過濾器的名字,如果沒有,它會以filterName過濾器名從spring容器中獲取。

輸入圖片說明

2.2 request的來源

前面說 DelegatingFilterProxy 會從spring容器中獲取名為 targetBeanName 的過濾器。接下來看下spring配置檔案,在這裡定義了一個shiro Filter的工廠 org.apache.shiro.spring.web.ShiroFilterFactoryBean。

 <beanid="shiroFilter"class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <propertyname="securityManager"ref="securityManager"/>
        <propertyname="loginUrl"value="/login"/>
        <propertyname="successUrl"value="/main"/>
        <propertyname="unauthorizedUrl"value="/unauthorized"/>
        <propertyname="filters">
            <map>
                <!--表單認證器-->
                <entrykey="authc"value-ref="formAuthenticationFilter"/>
            </map>
        </property>
        <propertyname="filterChainDefinitions">
            <value>
                <!-- 請求 logout地址,shiro去清除session-->
                /logout = logout
                /static/** = anon
                /** = authc
            </value>
        </property>
    </bean>

熟悉spring 的應該知道,bean的工廠是用來生產相關的bean,並把bean註冊到spring容器中的。通過檢視工廠bean的getObject方法,可知,委託類呼叫的filter型別是SpringShiroFilter。接下來我們看一下類圖,瞭解一下它們之間的關係。

輸入圖片說明

既然SpringShiroFilter屬於過濾器,那麼它肯定有一個doFilter方法,doFilter由它的父類 OncePerRequestFilter 實現。OncePerRequestFilter 在doFilter方法中,判斷是否在request中有"already filtered"這個屬性設定為true,如果有,則交給下一個過濾器,如果沒有就執行 doFilterInternal( ) 抽象方法。

doFilterInternal由AbstractShiroFilter類實現,即SpringShiroFilter的直屬父類實現。doFilterInternal 一些關鍵流程如下:

protectedvoiddoFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)throws ServletException, IOException {

            //包裝request/response
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

            //建立subject,其實建立的是Subject的代理類DelegatingSubject
            final Subject subject = createSubject(request, response);
            
             // 繼續執行過濾器鏈,此時的request/response是前面包裝好的request/response
            subject.execute(new Callable() {
                public Object call()throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
    }

在doFilterInternal中,可以看到對ServletRequest和ServletReponse進行了包裝。除此之外,還把包裝後的request/response作為引數,建立Subject,這個subject其實是代理類DelegatingSubject。

那麼,這個包裝後的request是什麼呢?我們繼續解析prepareServletRequest。

    protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain){
        ServletRequest toUse = request;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest http = (HttpServletRequest) request;
            toUse = wrapServletRequest(http);  //真正去包裝request的方法
        }
        return toUse;
    }

繼續包裝request,看下wrapServletRequest方法。無比興奮啊,文章前面的ShiroHttpServletRequest終於出來了,我們在controller中獲取到的request就是它,是它,它。它是servlet的HttpServletRequestWrapper的子類。

    protected ServletRequest wrapServletRequest(HttpServletRequest orig){
        //看看看,ShiroHttpServletRequest
        return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());  
    }

ShiroHttpServletRequest構造方法的第三個引數是個關鍵引數,我們先不管它怎麼來的,進ShiroHttpServletRequest裡面看看它有什麼用。它主要在兩個地方用到,一個是getRequestedSessionId(),這個是獲取sessionid的方法;另一個是getSession(),它是獲取session會話物件的。

先來看一下getRequestedSessionId()。isHttpSessions決定sessionid是否來自servlet。

public String getRequestedSessionId() {
        String requestedSessionId = null;
        if (isHttpSessions()) {
            requestedSessionId = super.getRequestedSessionId();   //從servlet中獲取sessionid
        } else {
            Object sessionId = getAttribute(REFERENCED_SESSION_ID);   //從request中獲取REFERENCED_SESSION_ID這個屬性
            if (sessionId != null) {
                requestedSessionId = sessionId.toString();
            }
        }

        return requestedSessionId;
    }

再看一下getSession()。isHttpSessions決定了session是否來自servlet。

    public HttpSession getSession(boolean create) {

        HttpSession httpSession;

        if (isHttpSessions()) {
            httpSession = super.getSession(false);  //從servletRequest獲取session
            if (httpSession == null && create) {
                if (WebUtils._isSessionCreationEnabled(this)) {
                    httpSession = super.getSession(create);  //從servletRequest獲取session
                } else {
                    throw newNoSessionCreationException();
                }
            }
        } else {
            if (this.session == null) {

                boolean existing = getSubject().getSession(false) != null; //從subject中獲取session

                Session shiroSession = getSubject().getSession(create); //從subject中獲取session
                if (shiroSession != null) {
                    this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
                    if (!existing) {
                        setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
                    }
                }
            }
            httpSession = this.session;
        }

        return httpSession;
    }

既然isHttpSessions()那麼重要,我們還是要看一下在什麼情況下,它返回true。

   protectedbooleanisHttpSessions(){
        return getSecurityManager().isHttpSessionMode();
    }

isHttpSessions是否返回true是由使用的shiro安全管理器的 isHttpSessionMode() 決定的。回到前面,我們使用的安全管理器是 DefaultWebSecurityManager ,我們看一下 DefaultWebSecurityManager 的原始碼,找到 isHttpSessionMode 方法。可以看到,SessionManager 的型別和 isServletContainerSessions() 起到了決定性的作用。

    publicbooleanisHttpSessionMode(){
        SessionManager sessionManager = getSessionManager();
        return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
    }

在配置檔案中,我們並沒有配置SessionManager ,安全管理器會使用預設的會話管理器 ServletContainerSessionManager,在 ServletContainerSessionManager 中,isServletContainerSessions 返回 true 。

因此,在前面的shiro配置的情況下,request中獲取的session將會是servlet context下的session。

2.3 subject的session來源

前面 doFilterInternal 的分析中,還落下了subject的建立過程,接下來我們解析該過程,從而揭開通過subject獲取session,這個session是從哪來的。

回憶下,在controller中怎麼通過subject獲取session。

 Subject currentUser = SecurityUtils.getSubject();
 Session session = currentUser.getSession(); // session的型別?

我們看一下shiro定義的session類圖,它們具有一些與 HttpSession 相同的方法,例如 setAttribute 和 getAttribute。

輸入圖片說明

還記得在 doFilterInternal 中,shiro把包裝後的request/response作為引數,建立subject嗎

final Subject subject = createSubject(request, response);

subject的建立時序圖

輸入圖片說明

最終,由 DefaultWebSubjectFactory 建立subject,並把 principals, session, request, response, securityManager這些引數封裝到subject。由於第一次建立session,此時session沒有例項。

那麼,當我們呼叫 subject .getSession() 嘗試獲取會話session時,發生了什麼呢。從前面的程式碼可以知道,我們獲取到的subject是 WebDelegatingSubject 型別的,它的父類 DelegatingSubject 實現了getSession 方法,下面的程式碼是getSession方法中的關鍵步驟。

 public Session getSession(boolean create) {
        if (this.session == null && create) {
            // 建立session上下文,上下文裡面封裝有request/response/host
            SessionContext sessionContext = createSessionContext();
            // 根據上下文,由securityManager建立session
            Session session = this.securityManager.start(sessionContext);
            // 包裝session
            this.session = decorate(session);
        }
        return this.session;
    }

接下來解析一下,安全管理器根據會話上下文建立session這個流程,追蹤程式碼後,可以知道它其實是交由 sessionManager 會話管理器進行會話建立,由前面的程式碼可以知道,這裡的sessionManager 其實是 ServletContainerSessionManager類,找到它的 createSession 方法。

 protected Session createSession(SessionContext sessionContext)throws AuthorizationException {
        HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);

        // 從request中獲取HttpSession
        HttpSession httpSession = request.getSession();

        String host = getHost(sessionContext);

        // 包裝成 HttpServletSession 
        return createSession(httpSession, host);
    }

這裡就可以知道,其實session是來源於 request 的 HttpSession,也就是說,來源於上一個過濾器中request的HttpSession。HttpSession 以成員變數的形式存在 HttpServletSession 中。回憶前面從安全管理器獲取 HttpServletSession 後,還呼叫 decorate() 裝飾該session,裝飾後的session型別是 StoppingAwareProxiedSession,HttpServletSession 是它的成員 。

在文章一開始的時候,通過debug就已經知道,當我們通過 subject.getSession() 獲取的就是 StoppingAwareProxiedSession,可見,這與前面分析的是一致的 。

那麼,當我們通過session.getAttribute和session.addAttribute時,StoppingAwareProxiedSession 做了什麼?它是由父類 ProxiedSession 實現 session.getAttribute和session.addAttribute 方法。我們看一下 ProxiedSession 相關原始碼。

  public Object getAttribute(Object key) throws InvalidSessionException {
        return delegate.getAttribute(key);
    }
    publicvoidsetAttribute(Object key, Object value) throws InvalidSessionException {
        delegate.setAttribute(key, value);
    }

可見,getAttribute 和 addAttribute 由委託類delegate完成,這裡的delegate就是HttpServletSession 。接下來看 HttpServletSession 的相關方法。

    public Object getAttribute(Object key)throws InvalidSessionException {
        try {
            return httpSession.getAttribute(assertString(key));
        } catch (Exception e) {
            throw new InvalidSessionException(e);
        }
    }

    publicvoidsetAttribute(Object key, Object value)throws InvalidSessionException {
        try {
            httpSession.setAttribute(assertString(key), value);
        } catch (Exception e) {
            throw new InvalidSessionException(e);
        }
    }

此處的httpSession就是之前從HttpServletRequest獲取的,也就是說,通過request.getSeesion()與subject.getSeesion()獲取session後,對session的操作是相同的。

結論

(1)controller中的request,在shiro過濾器中的doFilterInternal方法,將被包裝為ShiroHttpServletRequest 。

(2)在controller中,通過 request.getSession(_) 獲取會話 session ,該session到底來源servletRequest 還是由shiro管理並管理建立的會話,主要由 安全管理器 SecurityManager 和 SessionManager 會話管理器決定。

(3)不管是通過 request.getSession或者subject.getSession獲取到session,操作session,兩者都是等價的。在使用預設session管理器的情況下,操作session都是等價於操作HttpSession。

文章轉自:https://my.oschina.net/thinwonton/blog/979118