Shiro User Manual-Session Management

2015年5月24日 由 Creater 留言 »

1. Session Management
Shiro提供了安全框架所独有的特性:企业级session的解决方案。

1.1 Using Sessions
使用Subject获取Session:

Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute( "someKey", someValue);

和HttpServletRequest的API相似,Suject.getSession(boolean create)方法和HttpServletRequest.getSession(boolean create)有相同的逻辑:
a. 如果当前的Subject已经有Session,返回当前Session,参数boolean被忽略。
b. 如果当前的Subject没有Session,且参数boolean是true,创建新Session,并返回。
c. 如果当前的Subject没有Session,且参数boolean是false,不创建新Session,直接返回null。
有了Session,就可以进行任何操作,比如设置超时时间等。需要注意的是,此Session非HttpSession,也就是说,getSession 可以在非web环境下调用。

2. The SessionManager
SessionManager,是SecurityManager维护的核心组件,管理应用中所有的Session。默认的SecurityManager实现为DefaultSessionManager,它提供了企业级session管理的所有特性,像session验证,清理无效session等。可以通过securityManager的getSessionManager/setSessionManager方法获取和设置sessionManager,也可以使用INI文件配置:

[main]
...
sessionManager = com.foo.my.SessionManagerImplementation
securityManager.sessionManager = $sessionManager

需要注意的是,创建SessionManager相当复杂,Shiro默认提供的SessionManager可以满足大部分应用的需求。如果在web应用中,需要使用其他SessionManager实现。

2.1 Session Timeout
默认session的超时时间为30分钟,可以使用globalSessionTimeout设置一个全局的session超时时间。比如设置session超时时间为1小时:

main]
...
# 3,600,000 milliseconds = 1 hour
securityManager.sessionManager.globalSessionTimeout = 3600000

2.2 Per-Session Timeout
globalSessionTimeout的设置是全局的,即对所有的session有效。也可以对单独session设置超时时间,使用session的timeout属性即可,单位也是毫秒。

2.3 Session Listeners
Shiro支持SessionListener的概念以允许你监听Session事件。可以通过实现SessionListener接口或继承SessionListenerAdapter类,然后设置SessionManager的sessionListeners属性即可,可以配置一或多个监听器:

[main]
...
aSessionListener = com.foo.my.SessionListener
anotherSessionListener = com.foo.my.OtherSessionListener

securityManager.sessionManager.sessionListeners = $aSessionListener, $anotherSessionListener, etc.

需要注意的是,任何一个session事件的发生都会触发SessionListeners。

2.4 Session Storage
Session的创建和更新,需要将其内部的数据保存到”仓库里”,session失效,也需要从”仓库里”删除其内部数据。SessionManager的实现中,会委托SessionDAO组件进行”仓库”的CRUD操作。
可以自定义SessionDAO实现,将session数据存储在内存,文件系统,关系型数据库,NoSQL数据库,或任何其他地方(上面所说仓库,即指存储的位置),然后配置到SessionManager的属性里即可:

[main]
...
sessionDAO = com.foo.my.SessionDAO
securityManager.sessionManager.sessionDAO = $sessionDAO

以下是需要注意的地方:
配置的securityManager.sessionManager.sessionDAO = $sessionDAO,只有在使用Shiro自带的session管理器时才起作用。在web环境下,应用会使用Servlet容器的session管理器,而导致配置的SessionDAO无效。如果想在web应用中使用SessionDAO,先配置Shiro自带的session管理器:

[main]
...
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager

# Configure a SessionDAO and then set it:
securityManager.sessionManager.sessionDAO = $sessionDAO

Shiro自带的session管理器将session存储在内存里。这不适用于生产环境。

2.5 EHCache SessionDAO
如果不想实现自己的SessionDAO,推荐使用EHCache。EHCache将session信息存储在内存里,当内存达到极限时,会转存到磁盘。这样保证了在生产环境下,不会出现session丢失的现象。EHCache还可以缓存认证和授权的信息。如果想不依赖容器做session的集群,EHCache也是一个很好的选择。在EHCache基础上加入Terracotta,即可实现与容器无关的session集群缓存。使用EHCache很简单,首先需要保证classpath里有shiro-ehcache-.jar,在INI中配置:

[main]

sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
securityManager.sessionManager.sessionDAO = $sessionDAO

cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager = $cacheManager

最后一行,securityManager.cacheManager = $cacheManager,配置了Shiro需要的CacheManager。由于EnterpriseCacheSessionDAO实现了CacheManagerAware接口,CacheManager会自动传入SessionDAO里。当SessionManager让EnterpriseCacheSessionDAO存储一个Session时,它将使用EHCache去存储session信息。

2.6 EHCache Session Cache Configuration
默认情况下,Shiro使用指定的ehcache.xml进行session缓存的配置。查看默认的ehcache.xml文件:

<cache name="shiro-activeSessionCache"
       maxElementsInMemory="10000"
       overflowToDisk="true"
       eternal="true"
       timeToLiveSeconds="0"
       timeToIdleSeconds="0"
       diskPersistent="true"
       diskExpiryThreadIntervalSeconds="600"/>

如果想改变缓存的配置,需要定义相同的缓存键(cache entry)以满足Shiro的需求。也可以根据需求改变maxElementsInMemory的值。需要注意的是,至少要保证以下两个属性存在且不能更改:
a. overflowToDisk=”true” 这个属性保证了,当程序吃光内存时,sessions被保存到磁盘。
b. eternal=”true” 这个属性保证了缓存键(cache entry,即Session实例)永不过期,且不能被缓存自动删除。这个属性非常重要,因为Shiro的内部验证是基于一个定时程序,如果关闭这个属性,缓存可能会在Shiro不知的情形下清除无用的session,导致验证出现问题。

2.7 EHCache Session Cache Name
默认情况下,EnterpriseCacheSessionDAO会使用配置在ehcache.xml中缓存名为shiro-activeSessionCache的CacheManager。如果想改为其他键名,只需要保证ehcache.xml中的缓存名与INI文件中配置的相同即可:

[main]
...
sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionDAO.activeSessionsCacheName = myname
...

2.8 Custom Session IDs
默认的SessionDAO实现使用外部SessionIdGenerator组件产生Session ID。JavaUuidSessionIdGenerator为默认SessionIdGenerator的实现,使用UUIDs产生一个字符串格式的ID,它适用于生产环境。也可以通过实现SessionIdGenerator接口自定义session id的生成策略,然后配置到SessionDAO里即可:

...
sessionIdGenerator = com.my.session.SessionIdGenerator
securityManager.sessionManager.sessionDAO.sessionIdGenerator = $sessionIdGenerator

2.9 Session Validation & Scheduling
Session必须有效,无效的session会被删除。这保证了,随着时间的持久,存储空间不会被占满,且session不会被重用。由于性能的原因,只有在判断session是否过期或失效时才会被验证,这意味着,如果没有周期性的验证,session orphans(指僵尸session,下面会解释)将占满存储空间。一个普通的情形可以描述僵尸session:比如,一个用户登录一个系统,session被创建。如果用户没有注销而直接关闭浏览器,session还保存在服务器的存储空间里。SessionManager无法知道这个用户已经不在使用浏览器,那么这个session就无法被再次使用了,就变为僵尸session了。
如果不定期的清理僵尸session,它将占满存储空间。为了防止僵尸session占满空间,SessionManager的实现提供了SessionValidationScheduler的支持。SessionValidationScheduler负责周期性的验证session以达到清除僵尸session目的。

2.10 Default SessionValidationScheduler
SessionValidationScheduler的默认实现ExecutorServiceSessionValidationScheduler支持所有的环境,它使用JDK的ScheduledExecutorService去控制验证的频率。默认情况下,一小时验证一次。可以通过配置一个新的ExecutorServiceSessionValidationScheduler来改变验证的间隔,单位是毫秒:

[main]
...
sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler
# Default is 3,600,000 millis = 1 hour:
sessionValidationScheduler.interval = 3600000

securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler

2.11 Custom SessionValidationScheduler
也可以自定义SessionValidationScheduler实现,然后配置到SessionManager里即可:

[main]
...
sessionValidationScheduler = com.foo.my.SessionValidationScheduler
securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler

2.12 Disabling Session Validation
某些情况下,想关闭周期性验证session的功能,因为已在程序外控制了session的验证(比如通过企业级缓存的实时功能去清理过期的session,或使用自定义任务去验证),这种情况下,就需要关闭它:

[main]
...
securityManager.sessionManager.sessionValidationSchedulerEnabled = false

这只是关闭了Shiro周期性对session验证的功能,当session信息从存储空间取出时,依旧先执行验证操作。

2.13 Invalid Session Deletion
正如上面我们所说,周期性验证session的目的就是删除那些无效的session。默认情况下,只要Shiro检测到有无效的session,它就调用SessionDAO.delete(session)方法试图从存储空间删除这个session。这是保证应用的存储空间不被耗尽的最佳实践。然而,有些应用不想Shiro自动删除session。比如,应用程序提供了SessionDAO用于查询存储空间,需要查询那些过期和失效的session(像在过去的一周,一个用户创建了多少sessions,或者用户平均创建session的数量以及类似报告性查询),这种情况下,你需要完全的关闭session的删除功能:

[main]
...
securityManager.sessionManager.deleteInvalidSessions = false

需要注意的是,如果你配置Shiro让其不删除失效的sessions,为了保证存储空间不被耗尽,你需要自己删除那些失效的session。还有一点值得注意,关闭session删除和关闭session周期性验证是不一样的。

3. Session Clustering
让人振奋人心的是,Shiro自带的session支持session集群,且不依赖容器环境。也就是说,如果你使用Shiro自带的session并配置了session集群,可以在Jetty,Tomcat,Jboss,Geronimo或任何其他环境下发布。Shiro是如何做到的?由于Shiro的N层POJO结构,Session集群就像Session持久化那样简单。如果配置了带有集群功能的SessionDAO,SessionDAO就负责和集群应用进行交互。

3.1 Distributed Caches
分布式缓存,例如: Ehcache+Terracotta, GigaSpaces Oracle Coherence,和 Memcached以及大多数的缓存,已经解决了分布式数据的持久化问题。因此,在Shiro中配置session集群,就是简单的配置一下分布式缓存。需要注意的是,当使用分布式缓存进行session存储,需要保证以下两点中必须有一项为真:
a. 分布式缓存有集群级的内存供所有活跃的session。
b. 如果分布式缓存没有集群级的内存来保存所有活跃的session,必须支持磁盘溢出,以防止session丢失。
不能保证其中的一点,就会出现session丢失问题。

3.2 EnterpriseCacheSessionDAO
Shiro提供了可以将数据持久化到分布式缓存的SessionDAO实现-EnterpriseCacheSessionDAO,只需要把CacheManager或Cache配置到EnterpriseCacheSessionDAO里即可:

#This implementation would use your preferred distributed caching product's APIs:
activeSessionCache = my.org.apache.shiro.cache.CacheImplementation

sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionDAO.activeSessionCache = $activeSessionCache

securityManager.sessionManager.sessionDAO = $sessionDAO

尽管可以向SessionDAO里注入Cache实例,但通常情况下,是配置CacheManager,因为CacheManager可以满足所有缓存的需求,只需将CacheManager中使用的缓存名字配置到EnterpriseCacheSessionDAO里即可:

# This implementation would use your caching product's APIs:
cacheManager = my.org.apache.shiro.cache.CacheManagerImplementation

# Now configure the EnterpriseCacheSessionDAO and tell it what
# cache in the CacheManager should be used to store active sessions:
sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
# This is the default value.  Change it if your CacheManager configured a different name:
sessionDAO.activeSessionsCacheName = shiro-activeSessionsCache
# Now have the native SessionManager use that DAO:
securityManager.sessionManager.sessionDAO = $sessionDAO

# Configure the above CacheManager on Shiro's SecurityManager
# to use it for all of Shiro's caching needs:
securityManager.cacheManager = $cacheManager

注意到上面的配置有点奇怪么?我们没有配置sessionDAO使用哪个Cache或CacheManager,它是怎么发现缓存并使用它们的呢?Shiro在初始化SecurityManager时,它会检查SessionDAO是否实现了CacheManagerAware接口。如果实现,它就会自动使用配置的全局CacheManager。而EnterpriseCacheSessionDAO实现了CacheManagerAware接口,SecurityManager会把配置的cacheManager(securityManager.cacheManager = $cacheManager)通过参数的形式传给EnterpriseCacheSessionDAO的setCacheManager方法。运行时,如果EnterpriseCacheSessionDAO需要activeSessionsCache,它会在CacheManager中通过键-activeSessionsCache进行查找,得到Cache实例,并使用缓存。

3.3 Ehcache + Terracotta
一个众所周知的分布式缓存方案就是Ehcache + Terracotta。如果你对Terracotta + Ehcache的分布式很了解的话,Shiro这部分的配置就很简单。由于添加了Terracotta,需要在ehcache.xml文件中加入Terracotta的配置。以下这个配置经过测试,可以正确运行:

<ehcache>
    <terracottaConfig url="localhost:9510"/>
    <diskStore path="java.io.tmpdir/shiro-ehcache"/>
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="false"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120">
        <terracotta/>
   </defaultCache>
   <cache name="shiro-activeSessionCache"
       maxElementsInMemory="10000"
       eternal="true"
       timeToLiveSeconds="0"
       timeToIdleSeconds="0"
       diskPersistent="false"
       overflowToDisk="false"
       diskExpiryThreadIntervalSeconds="600">
       <terracotta/>
   </cache>
   <!-- Add more cache entries as desired, for example,
        Realm authc/authz caching: -->
</ehcache>

可以通过改变将引用指向你的Terracotta服务器。还有一点需要主要,和之前的配置不一样,必须将diskPersistent和overflowToDisk属性设为false,因为true在集群环境下不支持。
将这部分内容保存到ehcache.xml后,放在classpath路径下,然后打开Terracotta+Ehcache配置:

sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
# This name matches a cache name in ehcache.xml:
sessionDAO.activeSessionsCacheName = shiro-activeSessionsCache
securityManager.sessionManager.sessionDAO = $sessionDAO

# Configure The EhCacheManager:
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
cacheManager.cacheManagerConfigFile = classpath:ehcache.xml

# Configure the above CacheManager on Shiro's SecurityManager
# to use it for all of Shiro's caching needs:
securityManager.cacheManager = $cacheManager

需要记住,顺序很重要。通过把cacheManager配置在securityManager里,保证了所有之前配置的CacheManagerAware组件能够传入CacheManager。

4. Sessions and Subject State
a. Stateful Applications (Sessions allowed)
默认情况下,SecurityManager使用Session存储Subject的标识信息和认证状态,这有以下几种好处:
1. 所有的应用程序对服务的请求,调用都和session ID关联,这对于Shiro将用户和请求关联十分重要。比如,通过调用Subject.Builder获取其关联的Subject:

Serializable sessionId = //get from the inbound request or remote method invocation payload
Subject requestSubject = new Subject.Builder().sessionId(sessionId).buildSubject();

这对于web应用和开发远程调用框架或消息服务框架非常有用。

2. 任何在初始化请求”RememberMe”得到的标识都可以在第一次访问时保存到session里。这保证了Subject的remembered标识可以通过请求被保存而无需每次都反序列化或解码。比如,在web应用中,如果标识已存在session里,就无需每次请求时读取加密过的RememberMe cookie。这是一个不错的性能增强。

b. Stateless Applications (Sessionless)
对于大部分的应用来说,默认策略都可以满足,除了应用中有无状态的需求。好多无状态的架构不允许在请求时持久化状态。那么Subject状态就无法通过请求保存,这要求应用程序必须在其他地方保证Subject的状态。这种场景一般发生在,对每次请求、调用都需要进行验证时,比如很多无状态的应用通过强制进行HTTP的验证,让浏览器代替用户进行验证。

4.1 Disabling Subject State Session Storage
从Shiro的1.2及其以后版本,如果想关闭Subject的持久化,可以这样做:

[main]
...
securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
...

这将使Shiro无法从每次的请求/调用中使用session存储subject的状态。这意味着,你每次请求都需要认证,Shiro才能知道当前的Subject是谁。

4.2 A Hybrid Approach
上面的配置:securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false关闭了Shiro使用Session作为所有Subject存储的策略。假设存在某种情景,一些Subject的信息需要使用Session存储,而其他Subject不需要,怎么办呢?:
a. 也许和人相关的Subject需要使用Session存储。
b. 也许和人无关的Subject不需要使用Session存储。
c. 也许所有和特殊类型或者对访问系统指定的位置使用session存储,其他的不需要。
如果你需要混合的使用session存储策略,,可以实现SessionStorageEvaluator接口。

4.3 SessionStorageEvaluator
如果你想准确的控制哪些Subject需要他们的状态持久化到Session,可以实现SessionStorageEvaluator接口,这个接口只有一个方法:

public interface SessionStorageEvaluator {

    public boolean isSessionStorageEnabled(Subject subject);

}

4.4 Subject Inspection
当实现了SessionStorageEvaluator接口的isSessionStorageEnabled(subject)方法,可以对subject进行任何的操作,不过和应用环境相关的Subject才是最有用的。比如,在web应用中,如果数据必须基于ServletRequest存储,可以获取request或response,转换Subject实例(以为运行时Subject实例就为WebSubject实例),就会获取HttpServletRequest:

public boolean isSessionStorageEnabled(Subject subject) {
        boolean enabled = false;
        if (WebUtils.isWeb(Subject)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subject);
            //set 'enabled' based on the current request.
        } else {
            //not a web request - maybe a RMI or daemon invocation?
            //set 'enabled' another way...
        }

        return enabled;
    }

4.5 Configuration
实现了SessionStorageEvaluator接口,然后进行如下配置:

[main]
...
sessionStorageEvaluator = com.mycompany.shiro.subject.mgt.MySessionStorageEvaluator
securityManager.subjectDAO.sessionStorageEvaluator = $sessionStorageEvaluator
...

4.6 Web Applications
通常web应用希望每个请求可以容易地启用或禁用会话的创建,而不管哪个Subject 正在执行请求。这在支持REST,消息服务或RMI构架上起到很好的效果。比如,也许正常的终端用户允许创建和使用session,但是远程的API客户端使用REST或SOAP,没有session的概念。为了支持这种混合的/每请求的功能,Shiro将noSessionCreation过滤器添加到了默认的过滤器池中。这个过滤器可以防止新session的创建,以达到无状态的调用。在INI文件的[urls]部分,显式的在其他过滤器前定义,以确保session不被使用:

[urls]
...
/rest/** = noSessionCreation, authcBasic, ...

这个过滤器允许session使用当前的session,但不允许在过滤器请求范围内创建新session。也就是说,在一个没有存在session的request或subject内,下面任意四个方法的调用都将自动触发DisabledSessionException:
a. httpServletRequest.getSession()
b. httpServletRequest.getSession(true)
c. subject.getSession()
d. subject.getSession(true)
如果一个Subject在访问由noSessionCreation保护的url前已经有session存在,上面四个方法将正常执行。最后,在所有情况下,下面这两个方法都可以执行:
a. httpServletRequest.getSession(false)
b. subject.getSession(false)

广告位

发表评论

你必须 登陆 方可发表评论.