设为首页 收藏本站
查看: 2780|回复: 0

[经验分享] 【Apache Shiro】Session Management

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2014-5-28 08:48:09 | 显示全部楼层 |阅读模式
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,方法都是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    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)。
1
2
3
4
public interface SessionManager {
    Session start(SessionContext context);
    Session getSession(SessionKey key) throws SessionException;
}




Shiro为SessionManager提供了3个实现类(顺便也整理一下与SecurityManager实现类的关系)。
wKioL1OC_YigywRoAAH6Axdg5f8682.jpg
·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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    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();
1
2
3
4
5
6
7
8
    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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    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来执行。
1
2
3
    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())。
1
2
3
4
5
6
7
8
9
10
11
    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创建、停止、过期时按需进行操作。
1
2
3
4
5
6
7
8
public interface SessionListener {

    void onStart(Session session);

    void onStop(Session session);

    void onExpiration(Session session);
}




我只需要定义一个Listener并将它注入到sessionManager中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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...");
    }

}



1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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存储于任何一种数据源中。
1
2
3
4
5
6
7
8
9
10
11
12
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();
}



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

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

·CachingSessionDAO:为session存储提供透明的缓存支持,使用CacheManager维护缓存。
·EnterpriseCacheSessionDAO:通过匿名内部类重写了AbstractCacheManagercreateCache,返回MapCache对象。
·MemorySessionDAO:基于内存的实现,所有会话放在内存中。
下图中的匿名内部类就是EnterpriseCacheSessionDAOCacheManager
wKiom1OELaPTZPeDAABXGoyIYuU089.jpg

默认使用MemorySessionDAO(注意!DefaultWebSessionManager extends DefaultSessionManager)


当然,我们也可以试着使用缓存。
Shiro没有默认启用EHCache,但是为了保证session不会在运行时莫名其妙地丢失,建议启用EHCache优化session管理。
启用EHCache为session持久化服务非常简单,首先我们需要添加一个denpendency。
1
2
3
4
5
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>${shiro.version}</version>
</dependency>



接着只需要配置一下,以.ini配置为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[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";
来看一下他的配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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,其实现为:
1
2
3
    public Serializable generateId(Session session) {
        return UUID.randomUUID().toString();
    }



当然,我们也可以自己定制实现SessionIdGenerator。

Session Validation & Scheduling
比如说用户在浏览器上使用web应用时session被创建并缓存什么的都没有什么问题,只是用户退出的时候可以直接关掉浏览器、关掉电源、停电或者其他天灾什么的。然后session的状态就不得而知了(it is orphaned)。

为了防止垃圾被一点点堆积起来,我们需要周期性地检查session并在必要时删除session。
于是我们有SessionValidationScheduler:
1
2
3
4
5
6
7
public interface SessionValidationScheduler {

    boolean isEnabled();
    void enableSessionValidation();
    void disableSessionValidation();

}



Shiro只提供了一个实现,ExecutorServiceSessionValidationScheduler。
默认情况下,验证周期为60分钟。当然,我们也可以通过修改他的interval属性改变验证周期(单位为毫秒),比如这样:
1
2
3
sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler
sessionValidationScheduler.interval = 3600000
securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler



如果打算禁用按周期验证session(比如我们在Shiro外做了一些工作),则可以设置

1
securityManager.sessionManager.sessionValidationSchedulerEnabled = false



如果不打算删除失效的session(比如我们要做点统计之类的),则可以设置
1
securityManager.sessionManager.deleteInvalidSessions = false



运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-19777-1-1.html 上篇帖子: apache配置文件详解 下篇帖子: Apache 配置虚拟机时候DocumentRoot参数最后不要加斜杠
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表