session管理能夠說是Shiro的一大賣點。java
Shiro能夠爲任何應用(從簡單的命令行程序仍是手機應用再到大型企業應用)提供會話解決方案。
web
在Shiro出現以前,若是咱們想讓你的應用支持session,咱們一般會依賴web容器或者使用EJB的Session Bean。
apache
Shiro對session的支持更加易用,並且他能夠在任何應用、任何容器中使用。
瀏覽器
即使咱們使用Servlet或者EJB也並不表明咱們必須使用容器的session,Shiro提供的一些特性足以讓咱們用Shiro session替代他們。緩存
使用Shiro session時,不管是在JavaSE仍是web,方法都是同樣的。session
public static void main(String[] args) { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro/shiro.ini"); SecurityUtils.setSecurityManager(factory.getInstance()); Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("king","t;stmdtkg"); currentUser.login(token); Session session = currentUser.getSession(); System.out.println(session.getHost()); System.out.println(session.getId()); System.out.println(session.getStartTimestamp()); System.out.println(session.getLastAccessTime()); session.touch(); User u = new User(); session.setAttribute(u, "King."); Iterator<Object> keyItr = session.getAttributeKeys().iterator(); while(keyItr.hasNext()){ System.out.println(session.getAttribute(keyItr.next())); } }
不管是什麼環境,只須要調用Subject的getSession()便可。
less
另外Subject還提供了一個...dom
Session getSession(boolean create);
即,當前Subject的session不存在時是否建立並返回新的session。ide
以DelegatingSubject爲例:
(注意!從Shiro 1.2開始多了一個isSessionCreationEnabled屬性,其默認值爲true。)優化
public Session getSession() { return getSession(true); } public Session getSession(boolean create) { if (log.isTraceEnabled()) { log.trace("attempting to get session; create = " + create + "; session is null = " + (this.session == null) + "; session has id = " + (this.session != null && session.getId() != null)); } if (this.session == null && create) { //added in 1.2: if (!isSessionCreationEnabled()) { String msg = "Session creation has been disabled for the current subject. This exception indicates " + "that there is either a programming error (using a session when it should never be " + "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " + "for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " + "for more."; throw new DisabledSessionException(msg); } log.trace("Starting session for host {}", getHost()); SessionContext sessionContext = createSessionContext(); Session session = this.securityManager.start(sessionContext); this.session = decorate(session); } return this.session; }
正如其名,sessionManager用於爲應用中的Subject管理session,好比建立、刪除、失效或者驗證等。
和Shiro中的其餘核心組件同樣,他由SecurityManager維護。
(注意:public interface SecurityManager extends Authenticator, Authorizer, SessionManager)。
public interface SessionManager { Session start(SessionContext context); Session getSession(SessionKey key) throws SessionException; }
Shiro爲SessionManager提供了3個實現類(順便也整理一下與SecurityManager實現類的關係)。
其中ServletContainerSessionManager只適用於servlet容器中,若是須要支持多種客戶端訪問,則應該使用DefaultWebSessionManager。
默認狀況下,sessionManager的實現類的超時設爲30分鐘。
見AbstractSessionManager:
public static final long DEFAULT_GLOBAL_SESSION_TIMEOUT = 30 * MILLIS_PER_MINUTE; private long globalSessionTimeout = DEFAULT_GLOBAL_SESSION_TIMEOUT;
固然,咱們也能夠直接設置AbstractSessionManager的globalSessionTimeout。
好比在.ini中:
securityManager.sessionManager.globalSessionTimeout = 3600000
注意!若是使用的SessionManager是ServletContainerSessionManager(沒有繼承AbstractSessionManager),超時設置則依賴於Servlet容器的設置。
見: https://issues.apache.org/jira/browse/SHIRO-240
session過時的驗證方法能夠參考SimpleSession:
protected boolean isTimedOut() { if (isExpired()) { return true; } long timeout = getTimeout(); if (timeout >= 0l) { Date lastAccessTime = getLastAccessTime(); if (lastAccessTime == null) { String msg = "session.lastAccessTime for session with id [" + getId() + "] is null. This value must be set at " + "least once, preferably at least upon instantiation. Please check the " + getClass().getName() + " implementation and ensure " + "this value will be set (perhaps in the constructor?)"; throw new IllegalStateException(msg); } // Calculate at what time a session would have been last accessed // for it to be expired at this point. In other words, subtract // from the current time the amount of time that a session can // be inactive before expiring. If the session was last accessed // before this time, it is expired. long expireTimeMillis = System.currentTimeMillis() - timeout; Date expireTime = new Date(expireTimeMillis); return lastAccessTime.before(expireTime); } else { if (log.isTraceEnabled()) { log.trace("No timeout for session with id [" + getId() + "]. Session is not considered expired."); } } return false; }
試着從SecurityUtils.getSubject()一步步detect,感覺一下session是如何設置到subject中的。
判斷線程context中是否存在Subject後,若不存在,咱們使用Subject的內部類Builder進行buildSubject();
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
buildSubject()將創建Subject的工做委託給securityManager.createSubject(subjectContext)
createSubject會調用resolveSession處理session。
protected SubjectContext resolveSession(SubjectContext context) { if (context.resolveSession() != null) { log.debug("Context already contains a session. Returning."); return context; } try { //Context couldn't resolve it directly, let's see if we can since we have direct access to //the session manager: Session session = resolveContextSession(context); if (session != null) { context.setSession(session); } } catch (InvalidSessionException e) { log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " + "(session-less) Subject instance.", e); } return context; }
resolveSession(subjectContext),首先嚐試從context(MapContext)中獲取session,若是沒法直接獲取則改成獲取subject,再調用其getSession(false)。
若是仍不存在則調用resolveContextSession(subjectContext),試着從MapContext中獲取sessionId。
根據sessionId實例化一個SessionKey對象,並經過SessionKey實例獲取session。
getSession(key)的任務直接交給sessionManager來執行。
public Session getSession(SessionKey key) throws SessionException { return this.sessionManager.getSession(key); }
sessionManager.getSession(key)方法在AbstractNativeSessionManager中定義,該方法調用lookupSession(key)
,
lookupSession調用doGetSession(key)
,doGetSession(key)
是個protected abstract,實現由子類AbstractValidatingSessionManager提供。
doGetSession調用retrieveSession(key),該方法嘗試經過sessionDAO得到session信息。
最後,判斷session是否爲空後對其進行驗證(參考SimpleSession.validate())。
protected final Session doGetSession(final SessionKey key) throws InvalidSessionException { enableSessionValidationIfNecessary(); log.trace("Attempting to retrieve session with key {}", key); Session s = retrieveSession(key); if (s != null) { validate(s, key); } return s; }
咱們能夠經過SessionListener接口或者SessionListenerAdapter來進行session監聽,在session建立、中止、過時時按需進行操做。
public interface SessionListener { void onStart(Session session); void onStop(Session session); void onExpiration(Session session); }
我只須要定義一個Listener並將它注入到sessionManager中。
package pac.testcase.shiro.listener; import org.apache.shiro.session.Session; import org.apache.shiro.session.SessionListener; public class MySessionListener implements SessionListener { public void onStart(Session session) { System.out.println(session.getId()+" start..."); } public void onStop(Session session) { System.out.println(session.getId()+" stop..."); } public void onExpiration(Session session) { System.out.println(session.getId()+" expired..."); } }
[main] realm0=pac.testcase.shiro.realm.MyRealm0 realm1=pac.testcase.shiro.realm.MyRealm1 authcStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager #sessionManager = org.apache.shiro.web.session.mgt.ServletContainerSessionManager sessionListener = pac.testcase.shiro.listener.MySessionListener securityManager.realms=$realm1 securityManager.authenticator.authenticationStrategy = $authcStrategy securityManager.sessionManager=$sessionManager #sessionManager.sessionListeners =$sessionListener securityManager.sessionManager.sessionListeners=$sessionListener
SessionManager將session CRUD的工做委託給SessionDAO。
咱們能夠用特定的數據源API實現SessionDAO,以將session存儲於任何一種數據源中。
public interface SessionDAO { Serializable create(Session session); Session readSession(Serializable sessionId) throws UnknownSessionException; void update(Session session) throws UnknownSessionException; void delete(Session session); Collection<Session> getActiveSessions(); }
固然,也能夠把子類拿過去用。
下圖中的匿名內部類就是EnterpriseCacheSessionDAO的CacheManager。
默認使用MemorySessionDAO(注意!DefaultWebSessionManager extends DefaultSessionManager)
固然,咱們也能夠試着使用緩存。
Shiro沒有默認啓用EHCache,可是爲了保證session不會在運行時莫名其妙地丟失,建議啓用EHCache優化session管理。
啓用EHCache爲session持久化服務很是簡單,首先咱們須要添加一個denpendency。
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>${shiro.version}</version> </dependency>
接着只須要配置一下,以.ini配置爲例:
[main] realm0=pac.testcase.shiro.realm.MyRealm0 realm1=pac.testcase.shiro.realm.MyRealm1 authcStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager cacheManager=org.apache.shiro.cache.ehcache.EhCacheManager sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO #sessionManager = org.apache.shiro.web.session.mgt.ServletContainerSessionManager sessionListener = pac.testcase.shiro.listener.MySessionListener securityManager.realms=$realm1 securityManager.authenticator.authenticationStrategy = $authcStrategy securityManager.sessionManager=$sessionManager sessionManager.sessionListeners =$sessionListener sessionDAO.cacheManager=$cacheManager securityManager.sessionManager.sessionDAO=$sessionDAO securityManager.sessionManager.sessionListeners=$sessionListener
此處主要是cacheManager的定義和引用。
另外,此處使用的sessionDAO爲EnterpriseCacheSessionDAO。
前面說過EnterpriseCacheSessionDAO使用的CacheManager是基於MapCache的。
其實這樣設置並不會影響,由於EnterpriseCacheSessionDAO繼承CachingSessionDAO,CachingSessionDAO實現CacheManagerAware。
注意!只有在使用SessionManager的實現類時纔有sessionDAO屬性。
(事實上他們把sessionDAO定義在DefaultSessionManager中了,但彷佛有將sessionDAO放到AbstractValidatingSessionManager的打算。)
若是你在web應用中配置Shiro,啓動後你會驚訝地發現securityManger的sessionManager屬性竟然是ServletContainerSessionManager。
看一下上面的層次圖發現ServletContainerSessionManager和DefaultSessionManager沒有關係。
也就是說ServletContainerSessionManager不支持SessionDAO(cacheManger屬性定義在CachingSessionDAO)。
此時須要顯示指定sessionManager爲DefaultWebSessionManager。
關於EhCache的配置,默認狀況下EhCacheManager使用指定的配置文件,即:
private String cacheManagerConfigFile = "classpath:org/apache/shiro/cache/ehcache/ehcache.xml";
來看一下他的配置:
<ehcache> <diskStore path="java.io.tmpdir/shiro-ehcache"/> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> <cache name="shiro-activeSessionCache" maxElementsInMemory="10000" overflowToDisk="true" eternal="true" timeToLiveSeconds="0" timeToIdleSeconds="0" diskPersistent="true" diskExpiryThreadIntervalSeconds="600"/> <cache name="org.apache.shiro.realm.text.PropertiesRealm-0-accounts" maxElementsInMemory="1000" eternal="true" overflowToDisk="true"/> </ehcache>
若是打算改變該原有設置,其中有兩個屬性須要特別注意:
eternal="true":保證session緩存不會被自動失效,將其設爲false可能會和session validation的邏輯不符。
另外,name默認使用"shiro-activeSessionCache"
public static final String ACTIVESESSIONCACHE_NAME = "shiro-activeSessionCache";
若是打算使用其餘名字,只要在CachingSessionDAO或其子類設置activeSessionsCacheName便可。
當建立一個新的session時,SessionDAO的實現類使用SessionIdGenerator來爲session生成ID。
默認使用的SessionIdGenerator是JavaUuidSessionIdGenerator,其實現爲:
public Serializable generateId(Session session) { return UUID.randomUUID().toString(); }
固然,咱們也能夠本身定製實現SessionIdGenerator。
好比說用戶在瀏覽器上使用web應用時session被建立並緩存什麼的都沒有什麼問題,只是用戶退出的時候能夠直接關掉瀏覽器、關掉電源、停電或者其餘天災什麼的。
而後session的狀態就不得而知了(it is orphaned)。
爲了防止垃圾被一點點堆積起來,咱們須要週期性地檢查session並在必要時刪除session。
因而咱們有SessionValidationScheduler:
public interface SessionValidationScheduler { boolean isEnabled(); void enableSessionValidation(); void disableSessionValidation(); }
Shiro只提供了一個實現,ExecutorServiceSessionValidationScheduler。 默認狀況下,驗證週期爲60分鐘。
固然,咱們也能夠經過修改他的interval屬性改變驗證週期(單位爲毫秒),好比這樣:
sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler sessionValidationScheduler.interval = 3600000 securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler
若是打算禁用按週期驗證session(好比咱們在Shiro外作了一些工做),則能夠設置
securityManager.sessionManager.sessionValidationSchedulerEnabled = false
若是不打算刪除失效的session(好比咱們要作點統計之類的),則能夠設置
securityManager.sessionManager.deleteInvalidSessions = false