咱們在項目中使用了spring mvc做爲MVC框架,shiro做爲權限控制框架,在使用過程當中慢慢地產生了下面幾個疑惑,本篇文章將會帶着疑問慢慢地解析shiro源碼,從而解開內心面的那點小糾糾。java
(1)在spring controller中,request有何不一樣呢?web
因而,在controller中打印了request的類對象,發現request對象是org.apache.shiro.web.servlet.ShiroHttpServletRequest ,很明顯,此時的 request 已經被shiro包裝過了。spring
(2)衆所周知,spring mvc整合shiro後,能夠經過兩種方式獲取到session:apache
經過Spring mvc中controller的request獲取session安全
Session session = request.getSession();
經過shiro獲取sessionsession
Subject currentUser = SecurityUtils.getSubject(); Session session = currentUser.getSession();
那麼,問題來了,兩種方式獲取的session是否相同呢?mvc
這裏須要看一下項目中的shiro的securityManager配置,由於配置影響了shiro session的來源。這裏沒有配置session管理器。app
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!--<property name="sessionManager" ref="sessionManager"/>--> <property name="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呢?this
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容器中獲取。
前面說 DelegatingFilterProxy 會從spring容器中獲取名爲 targetBeanName 的過濾器。接下來看下spring配置文件,在這裏定義了一個shiro Filter的工廠 org.apache.shiro.spring.web.ShiroFilterFactoryBean。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/main"/> <property name="unauthorizedUrl" value="/unauthorized"/> <property name="filters"> <map> <!--表單認證器--> <entry key="authc" value-ref="formAuthenticationFilter"/> </map> </property> <property name="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 一些關鍵流程以下:
protected void doFilterInternal(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。
protected boolean isHttpSessions() { return getSecurityManager().isHttpSessionMode(); }
isHttpSessions是否返回true是由使用的shiro安全管理器的 isHttpSessionMode() 決定的。回到前面,咱們使用的安全管理器是 DefaultWebSecurityManager ,咱們看一下 DefaultWebSecurityManager 的源碼,找到 isHttpSessionMode 方法。能夠看到,SessionManager 的類型和 isServletContainerSessions() 起到了決定性的做用。
public boolean isHttpSessionMode() { SessionManager sessionManager = getSessionManager(); return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions(); }
在配置文件中,咱們並無配置SessionManager ,安全管理器會使用默認的會話管理器 ServletContainerSessionManager,在 ServletContainerSessionManager 中,isServletContainerSessions 返回 true 。
所以,在前面的shiro配置的狀況下,request中獲取的session將會是servlet context下的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); } public void setAttribute(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); } } public void setAttribute(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