shiro安全控制目錄
Shiro提供了完整的企業(yè)級會話管理功能,不依賴底層容器(如web容器的tomcat)机断,不管是JavaSE還是JavaEE環(huán)境都可以使用钳榨,提供了會話管理,會話監(jiān)聽州藕,會話存儲/持久化,容器無關的集群酝陈,失效/過期支持床玻。對Web的透明支持,SSO單點登錄的支持等特性沉帮。即使用Shiro的會話管理可以直接替換如Web容器的會話管理锈死。
序 什么叫做會話?
會話是用戶訪問應用時保持的連接關系穆壕。
因為http協(xié)議是無狀態(tài)的協(xié)議馅精,所以,需要借助會話(session)來使得應用在多次交互中能夠識別出當前訪問的用戶是誰粱檀。并且可以在多次會話中保存一些數據洲敢。
如訪問一些網站時登錄,網站可以記住用戶茄蚯,且在退出之前都可以識別當前用戶是誰压彭。
1. shiro Session簡單的API
Shiro Session和HttpSession使用方式很像。當然它們最大的區(qū)別在于你可以在任何應用中使用Shiro Session渗常,而不僅僅局限于Web應用壮不。
1. 獲取session對象
Shiro的會話支持不僅可以在普通JavaEE應用中使用,也可以在web應用中使用皱碘,且獲取方式是一致的询一。
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
//這個參數用于判定會話不存在時是否創(chuàng)建新會話。
Session session = subject.getSession(boolean create);
可以使用subject.getSession()獲取會話癌椿,其等價于subject.getSession(true)健蕊,即如果當前沒有創(chuàng)建Session對象,會創(chuàng)建一個踢俄。
2. 獲取會話的唯一標識
session.getId();
3. 獲取主機地址
session.getHost();
獲取當前subject的主機地址缩功,該地址是通過HostAuthenticationToken.getHost()
提供的遥诉。
4. 設置會話超時時間
//獲取超時時間
session.getTimeout();
//設置超時時間
session.setTimeout(毫秒);
獲取/設置當前Session的過期時間嗤堰;如果不設置是默認的會話管理器的全局過期時間。
5. 獲取啟動/訪問時間
//獲取會話的啟動時間
session.getStartTimestamp();
//獲取會話的最后訪問時間
session.getLastAccessTime();
獲取會話的啟動時間和最后訪問時間檬寂;如果是JavaSE應用需要自己定期調用session.touch()去更新最后訪問時間琳钉;如果是web應用势木,每次進入ShiroFilter都會自動調用session.touch()來更新最后訪問時間。
6. 更新/刪除會話
//更新會話最后訪問時間
session.touch();
//銷毀session會話
session.stop();
更新會話最后訪問時間及銷毀會話歌懒;當Subject.logout()時會自動調用stop方法來銷毀會話的啦桌。如果在web中,調用javax.servlet.http.HttpSession. invalidate()也會自動調用Shiro Session.stop方法進行銷毀Shiro的會話歼培。
7. 操作會話
session.setAttribute("key", "123");
Assert.assertEquals("123", session.getAttribute("key"));
session.removeAttribute("key");
設置/獲取/刪除會話屬性震蒋;在整個會話范圍內都可以對這些屬性進行操作。
SessionManager負責創(chuàng)建和管理用戶Session生命周期躲庄,在任何環(huán)境下都可以提供用戶健壯的session體驗查剖。默認情況下,Shiro會使用容器自帶的session機制噪窘,但若是容器不存在session笋庄,那么Shiro會提供內置的企業(yè)級session來管理。當然在開發(fā)中倔监,也可以使用SessionDAO允許數據源持久化Session直砂。
2. 會話管理器
在安全框架領域,Apache Shiro提供了一些獨特的東西浩习,可以在任何應用和架構層一致的使用Session API静暂,即Session不再依賴于Servlet或EJB容器。
Shiro會話最重要的一個好處便是它們獨立于容器谱秽。通過Shiro會話洽蛀,可以獲取一個容器無關的集群解決方案。Shiro架構允許可插拔的會話數據存儲疟赊,如企業(yè)緩存郊供,關系型數據庫,noSQL系統(tǒng)等近哟。并且Shiro會話可以跨客戶端技術進行共享驮审。
值得一提的是Shiro在Web環(huán)境中對會話的支持。
缺省 Http 會話
對于Web應用吉执,Shiro缺省將我們習以為常的Servlet容器會話作為其會話的基礎設施疯淫。即調用subject.getSession()
和subject.getSession(boolean)
方法時,Shiro會返回Servlet容器的HttpSession實例支持的Session實例戳玫。這種方式巧妙之處在于調用subject.getSession()的業(yè)務層代碼會跟一個Shiro Session實例交互峡竣,并且實際上它也會跟基于Web的HttpSession打交道×烤牛可以維護架構層之間的清晰隔離适掰。
Web 層中 Shiro 的原生會話
如果需要Shiro的企業(yè)級會話特性(如與容器無關的集群)而打開了Shiro的原生會話管理。而實際上我們也希望HttpServletRequest.getSession()
和HttpSession API
能和Shiro原生的會話協(xié)作荠列。而Shiro完整實現了Servlet規(guī)范中Session部分以及在Web應用中支持原生會話类浪。這意味著,不管何時你使用相應的HttpServletRequest或HttpSession調用肌似,Shiro都會將這些調用委托給內部的原生會話API费就。即無需修改Web代碼,即使正在使用Shiro內置的Session機制川队,獲取到的Servlet Session和Shiro Session依舊保持一致力细。
虛線:實現的接口睬澡;
實線:繼承的父類;
Shiro提供了三個默認實現:
- DefaultSessionManager:用于JavaSE環(huán)境眠蚂;
- ServletContainerSessionManager:用于Web環(huán)境煞聪,直接使用Servlet容器會話;
- DefaultWebSessionManager:用于Web環(huán)境逝慧,自己維護會話昔脯,不會使用Servlet容器的會話管理。
3. subject和request獲取Session的區(qū)別
3.1. 兩者方式獲取的session是否相同
1. 在Spring mvc中獲取session有兩種方法:
- 使用request對象獲取session
Session session = request.getSession();
- 通過shiro獲取session
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
一般在web中笛臣,有兩種會話管理器
- DefaultWebSessionManager (自己維護會話)
- ServletContainerSessionManager(默認云稚,直接使用servlet的會話)
而在項目中需要配置shiro的securityManager,因為配置影響了shiro session的來源沈堡。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroRealm"/>
</bean>
2. 兩種會話操縱的session是否相同静陈?(注:以Servlet會話進行分析)
在controller中打印session,發(fā)現request獲取的會話類型是:org.apache.catalina.session.StandardSessionFacade
诞丽,而subject的session類型是org.apache.shiro.subject.support.DelegatingSubject$StoppingAwareProxiedSession
在上圖中窿给,我們可以知道request獲取的session明顯是httpSession
,而subject獲取的session類型率拒,本質上也是httpSession
崩泡。即兩者在操作session時,都是操作的同一類型的session對象猬膨。
3.2 request對象中session的來源
- 如何獲取過濾器filter
SpringMVC整合shiro角撞,需要在web.xml中配置filter
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
DelegateFilterProxy是一個過濾器,準確來說是目的過濾器的代理勃痴,由它在doFilter方法中谒所,獲取spring容器中的過濾器,并調用目標過濾器的doFilter方法沛申。這樣做的好處是:原來的過濾器配置放在web.xml中劣领,現在可以把filter的配置放在spring中,并由spring管理它的生命周期铁材。
DelegatingFilterProxy——將Filter交由Spring管理
我們可以知道尖淘,使用DelegatingFilterProxy那么過濾器的生命周期由Spring來管理。若是沒有指定targetBeanName著觉,那么使用<filter-name>
- spring.xml配置
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,這個屬性是必須的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 要求登錄時的鏈接,非必須的屬性,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 -->
<property name="loginUrl" value="/login/init"/>
<!-- 用戶訪問未對其授權的資源時,所顯示的連接 -->
<property name="unauthorizedUrl" value="/pages/error/403.jsp"/>
<property name="filterChainDefinitions">
<value>
<!-- Shiro 過濾鏈的定義 -->
/login/init/** = anon
<!-- 對于登錄相關不進行鑒權 -->
/login/getVerifyCode/** = anon
<!-- 對于注冊相關不進行鑒權 -->
/register/** = anon
<!-- 靜態(tài)資源不進行鑒權 -->
/static/** = anon
</value>
</property>
<property name="filters">
<map>
<entry key="user" value-ref="userFilter"/>
</map>
</property>
</bean>
熟悉spring的應該知道村生,bean的工廠是用來生產相關的bean,并將bean注冊到spring容器中饼丘。通過查看工廠bean的getObject方法趁桃,可見,委托類調用的filter類型是SpringShiroFilter。
既然SpringShiroFilter屬于過濾器卫病,那么肯定有一個doFilter方法油啤,doFilter由它的父類OncePerRequestFilter實現。
OncePerRequestFilter在doFilter方法中蟀苛,判斷是否在request中有"already filtered"這個屬性設置為true益咬,如果有,則交給下一個過濾器屹逛,如果沒有,就執(zhí)行doFilterInternal()抽象方法汛骂。
doFilterInternal由AbstractShiroFilter類實現罕模,即SpringShiroFilter的直屬父類實現。
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);
//創(chuàng)建subject帘瞭,其實創(chuàng)建的是Subject的代理類DelegatingSubject
final Subject subject = createSubject(request, response);
// 繼續(xù)執(zhí)行過濾器鏈淑掌,此時的request/response是前面包裝好的request/response
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
}
在doFilterInternal中,可以看到對ServletRequest和ServletResponse進行了包裝蝶念,除此之外抛腕,還把包裝后的request/response作為參數,創(chuàng)建了Subject媒殉,這個subject其實就是代理類DelegatingSubject担敌。
那么這個包裝后的request是什么呢?
我們繼續(xù)解析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;
}
protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
//看看看全封,ShiroHttpServletRequest
return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
}
由此我們可以看到controller獲取到的ShiroHttpServletRequest對象。
ShiroHttpServletRequest構造方法的第三個參數是關鍵參數桃犬。進入ShiroHttpServletRequest里面看看它有什么用刹悴?
- 在getRequestedSessionId()方法用到,獲取sessionId攒暇。
- 在getSession()用到土匀,獲取session會話對象。
(1)先看下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;
}
(2)再看下getSession。isHttpSession決定了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()方法。
public boolean isHttpSessionMode() {
SessionManager sessionManager = getSessionManager();
return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
}
需要注意的是:在配置文件中梦鉴,我們并沒有配置SessionManager李茫,安全管理器會使用會話管理器ServletContainerSessionManager,在ServletContainerSessionManager中肥橙,isServletContainerSessions返回true魄宏。
因此,在前面的配置中存筏,request中獲取的session將是servlet context下的session宠互。
3.3. subject的session來源
前面的doFilterInternal的分析中,還提到了subject創(chuàng)建的過程椭坚。接著我們繼續(xù)分析該過程予跌,判斷subject中的session的來源。
在controller中subject獲取session
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
我們看下shiro定義的session類圖善茎,具有一些與HttpSession相同的方法券册,例如setAttribute和getAttribute。
在doFilterInternal中垂涯,shiro把包裝后的request/response作為參數烁焙,創(chuàng)建subject
final Subject subject = createSubject(request, response);
最終,由DefaultWebSubjectFactory創(chuàng)建subject耕赘,并把principals [資本 普瑞色跑死]
, session, request, response, securityManager參數封裝到subject骄蝇。由于第一次創(chuàng)建session,此時session沒有實例操骡。
那么乞榨,當我們第一次調用subject.getSession()嘗試獲取session時,發(fā)生了什么当娱?從前面的代碼我們知道吃既,我們獲取到的subject是WebDelegatingSubject類型,它的父類DelegatingSubject實現了getSession方法跨细。
public Session getSession(boolean create) {
if (this.session == null && create) {
// 創(chuàng)建session上下文鹦倚,上下文里面封裝有request/response/host
SessionContext sessionContext = createSessionContext();
// 根據上下文,由securityManager創(chuàng)建session
Session session = this.securityManager.start(sessionContext);
// 包裝session
this.session = decorate(session);
}
return this.session;
}
接下來解析一下冀惭,securityManager根據sessionContext 創(chuàng)建session這個流程震叙。它是交由sessionManager會話管理器進行會話創(chuàng)建。這里的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就是它的成員世澜。
session的getAttribute和addAttribute方法独旷,StoppingAwareProxiedSession做了些什么?
它是由父類ProxiedSession實現session.getAttribute和session.addAttribute方法寥裂。
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封恰。
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);
}
}
最后總結一下麻养,通過request.getSeesion()與subject.getSeesion()獲取session后,對session的操作是相同的俭驮。而session的來源是servletRequest還是shiro回溺。主要是由安全管理器SecurityManager和SessionManager會話管理器決定春贸。