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实现类的关系)。

session

・ 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 ;

当然,我们也可以直接设置 AbstractSessionManagerglobalSessionTimeout 。比如在.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 提供( final )。

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

当然,也可以把子类拿过去用。

session

・ AbstractSessionDAO :在 createread 时对session做验证,保证session可用,并提供了sessionId的生成方法。

・ CachingSessionDAO :为session存储提供透明的缓存支持,使用 CacheManager 维护缓存。

・ EnterpriseCacheSessionDAO :通过匿名内部类重写了 AbstractCacheManagercreateCache ,返回 MapCache 对象。

・ MemorySessionDAO :基于内存的实现,所有会话放在内存中。

下图中的匿名内部类就是 EnterpriseCacheSessionDAOCacheManager

session

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

看一下上面的层次图发现 ServletContainerSessionManagerDefaultSessionManager 没有关系。

也就是说 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 ACTIVE_SESSION_CACHE_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

相关推荐