Shiro - 關於session


Shiro Session

session管理可以說是Shiro的一大賣點。

 

Shiro可以為任何應用(從簡單的命令行程序還是手機應用再到大型企業應用)提供會話解決方案。

在Shiro出現之前,如果我們想讓你的應用支持session,我們通常會依賴web容器或者使用EJB的Session Bean。

Shiro對session的支持更加易用,而且他可以在任何應用、任何容器中使用。

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

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

使用Shiro session時,無論是在JavaSE還是web,方法都是一樣的。

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()即可。

另外Subject還提供了一個...

Session getSession(boolean create);

即,當前Subject的session不存在時是否創建並返回新的session。


以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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM