Shiro - 關於session

Shiro Session

session管理能夠說是Shiro的一大賣點。java

 

Shiro能夠爲任何應用(從簡單的命令行程序仍是手機應用再到大型企業應用)提供會話解決方案。
web

在Shiro出現以前,若是咱們想讓你的應用支持session,咱們一般會依賴web容器或者使用EJB的Session Bean。
apache

Shiro對session的支持更加易用,並且他能夠在任何應用、任何容器中使用。
瀏覽器

即使咱們使用Servlet或者EJB也並不表明咱們必須使用容器的session,Shiro提供的一些特性足以讓咱們用Shiro session替代他們。緩存

  • 基於POJO
  • 易定製session持久化
  • 容器無關的session集羣
  • 支持多種客戶端訪問
  • 會話事件監聽
  • 對失效session的延長
  • 對Web的透明支持
  • 支持SSO

使用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

正如其名,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實現類的關係)。

  • DefaultSessionManager
  • DefaultWebSessionManager
  • ServletContainerSessionManager

其中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;
}

 

Session Listener

咱們能夠經過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

 

SessionDAO

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();
}


固然,也能夠把子類拿過去用。

  • AbstractSessionDAO:在create和read時對session作驗證,保證session可用,並提供了sessionId的生成方法。
  • CachingSessionDAO:爲session存儲提供透明的緩存支持,使用CacheManager維護緩存。
  • EnterpriseCacheSessionDAO:經過匿名內部類重寫了AbstractCacheManager的createCache,返回MapCache對象。
  • MemorySessionDAO:基於內存的實現,全部會話放在內存中。

 

下圖中的匿名內部類就是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>

 

若是打算改變該原有設置,其中有兩個屬性須要特別注意:

  • overflowToDisk="true":保證session不會丟失。
  • 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。

 

Session Validation & Scheduling

好比說用戶在瀏覽器上使用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
相關文章
相關標籤/搜索