10 會話管理

Shiro提供了完整的企業級會話管理功能,不依賴於底層容器(如web容器tomcat),不管JavaSE還是JavaEE環境都可以使用,提供了會話管理、會話事件監聽、會話存儲/持久化、容器無關的集羣、失效/過期支持、對Web的透明支持、SSO單點登錄的支持等特性。即直接使用Shiro的會話管理可以直接替換如Web容器的會話管理。

 

會話

所謂會話,即用戶訪問應用時保持的連接關係,在多次交互中應用能夠識別出當前訪問的用戶是誰,且可以在多次交互中保存一些數據。如訪問一些網站時登錄成功後,網站可以記住用戶,且在退出之前都可以識別當前用戶是誰。

 

 

Shiro的會話支持不僅可以在普通的JavaSE應用中使用,也可以在JavaEE應用中使用,如web應用。且使用方式是一致的。 

Java代碼  收藏代碼

  1. login("classpath:shiro.ini""zhang""123");  
  2. Subject subject = SecurityUtils.getSubject();  
  3. Session session = subject.getSession();   

登錄成功後使用Subject.getSession()即可獲取會話;其等價於Subject.getSession(true),即如果當前沒有創建Session對象會創建一個;另外Subject.getSession(false),如果當前沒有創建Session則返回null(不過默認情況下如果啓用會話存儲功能的話在創建Subject時會主動創建一個Session)。

 

Java代碼  收藏代碼

  1. session.getId();  

獲取當前會話的唯一標識。

  

Java代碼  收藏代碼

  1. session.getHost();  

獲取當前Subject的主機地址,該地址是通過HostAuthenticationToken.getHost()提供的。 

 

Java代碼  收藏代碼

  1. session.getTimeout();  
  2. session.setTimeout(毫秒);   

獲取/設置當前Session的過期時間;如果不設置默認是會話管理器的全局過期時間。

  

Java代碼  收藏代碼

  1. session.getStartTimestamp();  
  2. session.getLastAccessTime();  

獲取會話的啓動時間及最後訪問時間;如果是JavaSE應用需要自己定期調用session.touch()去更新最後訪問時間;如果是Web應用,每次進入ShiroFilter都會自動調用session.touch()來更新最後訪問時間。    

 

Java代碼  收藏代碼

  1. session.touch();  
  2. session.stop();   

更新會話最後訪問時間及銷燬會話;當Subject.logout()時會自動調用stop方法來銷燬會話。如果在web中,調用javax.servlet.http.HttpSession. invalidate()也會自動調用Shiro Session.stop方法進行銷燬Shiro的會話。 

 

Java代碼  收藏代碼

  1. session.setAttribute("key""123");  
  2. Assert.assertEquals("123", session.getAttribute("key"));  
  3. session.removeAttribute("key");  

設置/獲取/刪除會話屬性;在整個會話範圍內都可以對這些屬性進行操作。 

 

Shiro提供的會話可以用於JavaSE/JavaEE環境,不依賴於任何底層容器,可以獨立使用,是完整的會話模塊。

 

會話管理器

會話管理器管理着應用中所有Subject的會話的創建、維護、刪除、失效、驗證等工作。是Shiro的核心組件,頂層組件SecurityManager直接繼承了SessionManager,且提供了SessionsSecurityManager實現直接把會話管理委託給相應的SessionManager,DefaultSecurityManager及DefaultWebSecurityManager默認SecurityManager都繼承了SessionsSecurityManager。

 

 

SecurityManager提供瞭如下接口:

 

Java代碼  收藏代碼

  1. Session start(SessionContext context); //啓動會話  
  2. Session getSession(SessionKey key) throws SessionException; //根據會話Key獲取會話   

 

另外用於Web環境的WebSessionManager又提供瞭如下接口:

 

Java代碼  收藏代碼

  1. boolean isServletContainerSessions();//是否使用Servlet容器的會話  

 

Shiro還提供了ValidatingSessionManager用於驗資並過期會話: 

Java代碼  收藏代碼

  1. void validateSessions();//驗證所有會話是否過期  


 

Shiro提供了三個默認實現:

DefaultSessionManager:DefaultSecurityManager使用的默認實現,用於JavaSE環境;

ServletContainerSessionManager:DefaultWebSecurityManager使用的默認實現,用於Web環境,其直接使用Servlet容器的會話;

DefaultWebSessionManager:用於Web環境的實現,可以替代ServletContainerSessionManager,自己維護着會話,直接廢棄了Servlet容器的會話管理。

 

替換SecurityManager默認的SessionManager可以在ini中配置(shiro.ini):

 

Java代碼  收藏代碼

  1. [main]  
  2. sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager  
  3. securityManager.sessionManager=$sessionManager   

 

 

Web環境下的ini配置(shiro-web.ini):

 

<!--EndFragment-->

 

Java代碼  收藏代碼

  1. [main]  
  2. sessionManager=org.apache.shiro.web.session.mgt.ServletContainerSessionManager  
  3. securityManager.sessionManager=$sessionManager  

   

 

另外可以設置會話的全局過期時間(毫秒爲單位),默認30分鐘:

 

Java代碼  收藏代碼

  1. sessionManager. globalSessionTimeout=1800000   

 

默認情況下globalSessionTimeout將應用給所有Session。可以單獨設置每個Session的timeout屬性來爲每個Session設置其超時時間。

 

另外如果使用ServletContainerSessionManager進行會話管理,Session的超時依賴於底層Servlet容器的超時時間,可以在web.xml中配置其會話的超時時間(分鐘爲單位): 

Java代碼  收藏代碼

  1. <session-config>  
  2.   <session-timeout>30</session-timeout>  
  3. </session-config>  

  

在Servlet容器中,默認使用JSESSIONID Cookie維護會話,且會話默認是跟容器綁定的;在某些情況下可能需要使用自己的會話機制,此時我們可以使用DefaultWebSessionManager來維護會話:

Java代碼  收藏代碼

  1. sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie  
  2. sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager  
  3. sessionIdCookie.name=sid  
  4. #sessionIdCookie.domain=sishuok.com  
  5. #sessionIdCookie.path=  
  6. sessionIdCookie.maxAge=1800  
  7. sessionIdCookie.httpOnly=true  
  8. sessionManager.sessionIdCookie=$sessionIdCookie  
  9. sessionManager.sessionIdCookieEnabled=true  
  10. securityManager.sessionManager=$sessionManager   

sessionIdCookie是sessionManager創建會話Cookie的模板:

sessionIdCookie.name:設置Cookie名字,默認爲JSESSIONID;

sessionIdCookie.domain:設置Cookie的域名,默認空,即當前訪問的域名;

sessionIdCookie.path:設置Cookie的路徑,默認空,即存儲在域名根下;

sessionIdCookie.maxAge:設置Cookie的過期時間,秒爲單位,默認-1表示關閉瀏覽器時過期Cookie;

sessionIdCookie.httpOnly:如果設置爲true,則客戶端不會暴露給客戶端腳本代碼,使用HttpOnly cookie有助於減少某些類型的跨站點腳本攻擊;此特性需要實現了Servlet 2.5 MR6及以上版本的規範的Servlet容器支持;

sessionManager.sessionIdCookieEnabled:是否啓用/禁用Session Id Cookie,默認是啓用的;如果禁用後將不會設置Session Id Cookie,即默認使用了Servlet容器的JSESSIONID,且通過URL重寫(URL中的“;JSESSIONID=id”部分)保存Session Id。

 

另外我們可以如“sessionManager. sessionIdCookie.name=sid”這種方式操作Cookie模板。

 

會話監聽器

會話監聽器用於監聽會話創建、過期及停止事件: 

Java代碼  收藏代碼

  1. public class MySessionListener1 implements SessionListener {  
  2.     @Override  
  3.     public void onStart(Session session) {//會話創建時觸發  
  4.         System.out.println("會話創建:" + session.getId());  
  5.     }  
  6.     @Override  
  7.     public void onExpiration(Session session) {//會話過期時觸發  
  8.         System.out.println("會話過期:" + session.getId());  
  9.     }  
  10.     @Override  
  11.     public void onStop(Session session) {//退出/會話過期時觸發  
  12.         System.out.println("會話停止:" + session.getId());  
  13.     }    
  14. }  

 

如果只想監聽某一個事件,可以繼承SessionListenerAdapter實現:

Java代碼  收藏代碼

  1. public class MySessionListener2 extends SessionListenerAdapter {  
  2.     @Override  
  3.     public void onStart(Session session) {  
  4.         System.out.println("會話創建:" + session.getId());  
  5.     }  
  6. }  

 

在shiro-web.ini配置文件中可以進行如下配置設置會話監聽器:

Java代碼  收藏代碼

  1. sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1  
  2. sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2  
  3. sessionManager.sessionListeners=$sessionListener1,$sessionListener2  

 

會話存儲/持久化 

Shiro提供SessionDAO用於會話的CRUD,即DAO(Data Access Object)模式實現:

Java代碼  收藏代碼

  1. //如DefaultSessionManager在創建完session後會調用該方法;如保存到關係數據庫/文件系統/NoSQL數據庫;即可以實現會話的持久化;返回會話ID;主要此處返回的ID.equals(session.getId());  
  2. Serializable create(Session session);  
  3. //根據會話ID獲取會話  
  4. Session readSession(Serializable sessionId) throws UnknownSessionException;  
  5. //更新會話;如更新會話最後訪問時間/停止會話/設置超時時間/設置移除屬性等會調用  
  6. void update(Session session) throws UnknownSessionException;  
  7. //刪除會話;當會話過期/會話停止(如用戶退出時)會調用  
  8. void delete(Session session);  
  9. //獲取當前所有活躍用戶,如果用戶量多此方法影響性能  
  10. Collection<Session> getActiveSessions();   

Shiro內嵌瞭如下SessionDAO實現:

AbstractSessionDAO提供了SessionDAO的基礎實現,如生成會話ID等;CachingSessionDAO提供了對開發者透明的會話緩存的功能,只需要設置相應的CacheManager即可;MemorySessionDAO直接在內存中進行會話維護;而EnterpriseCacheSessionDAO提供了緩存功能的會話維護,默認情況下使用MapCache實現,內部使用ConcurrentHashMap保存緩存的會話。

 

可以通過如下配置設置SessionDAO:

Java代碼  收藏代碼

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

Shiro提供了使用Ehcache進行會話存儲,Ehcache可以配合TerraCotta實現容器無關的分佈式集羣。

 

首先在pom.xml裏添加如下依賴:

Java代碼  收藏代碼

  1. <dependency>  
  2.     <groupId>org.apache.shiro</groupId>  
  3.     <artifactId>shiro-ehcache</artifactId>  
  4.     <version>1.2.2</version>  
  5. </dependency>   

 

接着配置shiro-web.ini文件:    

Java代碼  收藏代碼

  1. sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO  
  2. sessionDAO. activeSessionsCacheName=shiro-activeSessionCache  
  3. sessionManager.sessionDAO=$sessionDAO  
  4. cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager  
  5. cacheManager.cacheManagerConfigFile=classpath:ehcache.xml  
  6. securityManager.cacheManager = $cacheManager   

sessionDAO. activeSessionsCacheName:設置Session緩存名字,默認就是shiro-activeSessionCache;

cacheManager:緩存管理器,用於管理緩存的,此處使用Ehcache實現;

cacheManager.cacheManagerConfigFile:設置ehcache緩存的配置文件;

securityManager.cacheManager:設置SecurityManager的cacheManager,會自動設置實現了CacheManagerAware接口的相應對象,如SessionDAO的cacheManager;

 

然後配置ehcache.xml:

Java代碼  收藏代碼

  1. <cache name="shiro-activeSessionCache"  
  2.        maxEntriesLocalHeap="10000"  
  3.        overflowToDisk="false"  
  4.        eternal="false"  
  5.        diskPersistent="false"  
  6.        timeToLiveSeconds="0"  
  7.        timeToIdleSeconds="0"  
  8.        statistics="true"/>   

Cache的名字爲shiro-activeSessionCache,即設置的sessionDAO的activeSessionsCacheName屬性值。

 

另外可以通過如下ini配置設置會話ID生成器:

Java代碼  收藏代碼

  1. sessionIdGenerator=org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator  
  2. sessionDAO.sessionIdGenerator=$sessionIdGenerator   

用於生成會話ID,默認就是JavaUuidSessionIdGenerator,使用java.util.UUID生成。

 

如果自定義實現SessionDAO,繼承CachingSessionDAO即可:

Java代碼  收藏代碼

  1. public class MySessionDAO extends CachingSessionDAO {  
  2.     private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate();  
  3.      protected Serializable doCreate(Session session) {  
  4.         Serializable sessionId = generateSessionId(session);  
  5.         assignSessionId(session, sessionId);  
  6.         String sql = "insert into sessions(id, session) values(?,?)";  
  7.         jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session));  
  8.         return session.getId();  
  9.     }  
  10. protected void doUpdate(Session session) {  
  11.     if(session instanceof ValidatingSession && !((ValidatingSession)session).isValid()) {  
  12.         return//如果會話過期/停止 沒必要再更新了  
  13.     }  
  14.         String sql = "update sessions set session=? where id=?";  
  15.         jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId());  
  16.     }  
  17.     protected void doDelete(Session session) {  
  18.         String sql = "delete from sessions where id=?";  
  19.         jdbcTemplate.update(sql, session.getId());  
  20.     }  
  21.     protected Session doReadSession(Serializable sessionId) {  
  22.         String sql = "select session from sessions where id=?";  
  23.         List<String> sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId);  
  24.         if(sessionStrList.size() == 0return null;  
  25.         return SerializableUtils.deserialize(sessionStrList.get(0));  
  26.     }  
  27. }   

doCreate/doUpdate/doDelete/doReadSession分別代表創建/修改/刪除/讀取會話;此處通過把會話序列化後存儲到數據庫實現;接着在shiro-web.ini中配置:

Java代碼  收藏代碼

  1. sessionDAO=com.github.zhangkaitao.shiro.chapter10.session.dao.MySessionDAO  

其他設置和之前一樣,因爲繼承了CachingSessionDAO;所有在讀取時會先查緩存中是否存在,如果找不到纔到數據庫中查找。

 

會話驗證

Shiro提供了會話驗證調度器,用於定期的驗證會話是否已過期,如果過期將停止會話;出於性能考慮,一般情況下都是獲取會話時來驗證會話是否過期並停止會話的;但是如在web環境中,如果用戶不主動退出是不知道會話是否過期的,因此需要定期的檢測會話是否過期,Shiro提供了會話驗證調度器SessionValidationScheduler來做這件事情。

 

可以通過如下ini配置開啓會話驗證:    

Java代碼  收藏代碼

  1. sessionValidationScheduler=org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler  
  2. sessionValidationScheduler.interval = 3600000  
  3. sessionValidationScheduler.sessionManager=$sessionManager  
  4. sessionManager.globalSessionTimeout=1800000  
  5. sessionManager.sessionValidationSchedulerEnabled=true  
  6. sessionManager.sessionValidationScheduler=$sessionValidationScheduler   

sessionValidationScheduler:會話驗證調度器,sessionManager默認就是使用ExecutorServiceSessionValidationScheduler,其使用JDK的ScheduledExecutorService進行定期調度並驗證會話是否過期;

sessionValidationScheduler.interval:設置調度時間間隔,單位毫秒,默認就是1小時;

sessionValidationScheduler.sessionManager:設置會話驗證調度器進行會話驗證時的會話管理器;

sessionManager.globalSessionTimeout:設置全局會話超時時間,默認30分鐘,即如果30分鐘內沒有訪問會話將過期;

sessionManager.sessionValidationSchedulerEnabled:是否開啓會話驗證器,默認是開啓的;

sessionManager.sessionValidationScheduler:設置會話驗證調度器,默認就是使用ExecutorServiceSessionValidationScheduler。

 

Shiro也提供了使用Quartz會話驗證調度器:

Java代碼  收藏代碼

  1. sessionValidationScheduler=org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler  
  2. sessionValidationScheduler.sessionValidationInterval = 3600000  
  3. sessionValidationScheduler.sessionManager=$sessionManager   

使用時需要導入shiro-quartz依賴:

Java代碼  收藏代碼

  1. <dependency>  
  2.      <groupId>org.apache.shiro</groupId>  
  3.      <artifactId>shiro-quartz</artifactId>  
  4.      <version>1.2.2</version>  
  5. </dependency>  

    

如上會話驗證調度器實現都是直接調用AbstractValidatingSessionManager 的validateSessions方法進行驗證,其直接調用SessionDAO的getActiveSessions方法獲取所有會話進行驗證,如果會話比較多,會影響性能;可以考慮如分頁獲取會話並進行驗證,如com.github.zhangkaitao.shiro.chapter10.session.scheduler.MySessionValidationScheduler:

Java代碼  收藏代碼

  1. //分頁獲取會話並驗證  
  2. String sql = "select session from sessions limit ?,?";  
  3. int start = 0//起始記錄  
  4. int size = 20//每頁大小  
  5. List<String> sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);  
  6. while(sessionList.size() > 0) {  
  7.   for(String sessionStr : sessionList) {  
  8.     try {  
  9.       Session session = SerializableUtils.deserialize(sessionStr);  
  10.       Method validateMethod =   
  11.         ReflectionUtils.findMethod(AbstractValidatingSessionManager.class,   
  12.             "validate", Session.class, SessionKey.class);  
  13.       validateMethod.setAccessible(true);  
  14.       ReflectionUtils.invokeMethod(validateMethod,   
  15.         sessionManager, session, new DefaultSessionKey(session.getId()));  
  16.     } catch (Exception e) {  
  17.         //ignore  
  18.     }  
  19.   }  
  20.  start = start + size;  
  21.   sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);  
  22. }   

其直接改造自ExecutorServiceSessionValidationScheduler,如上代碼是驗證的核心代碼,可以根據自己的需求改造此驗證調度器器;ini的配置和之前的類似。

 

如果在會話過期時不想刪除過期的會話,可以通過如下ini配置進行設置:

Java代碼  收藏代碼

  1. sessionManager.deleteInvalidSessions=false  

默認是開啓的,在會話過期後會調用SessionDAO的delete方法刪除會話:如會話時持久化存儲的,可以調用此方法進行刪除。

 

如果是在獲取會話時驗證了會話已過期,將拋出InvalidSessionException;因此需要捕獲這個異常並跳轉到相應的頁面告訴用戶會話已過期,讓其重新登錄,如可以在web.xml配置相應的錯誤頁面:

Java代碼  收藏代碼

  1. <error-page>  
  2.     <exception-type>org.apache.shiro.session.InvalidSessionException</exception-type>  
  3.     <location>/invalidSession.jsp</location>  
  4. </error-page>  

 

sessionFactory

sessionFactory是創建會話的工廠,根據相應的Subject上下文信息來創建會話;默認提供了SimpleSessionFactory用來創建SimpleSession會話。

 

首先自定義一個Session:

Java代碼  收藏代碼

  1. public class OnlineSession extends SimpleSession {  
  2.     public static enum OnlineStatus {  
  3.         on_line("在線"), hidden("隱身"), force_logout("強制退出");  
  4.         private final String info;  
  5.         private OnlineStatus(String info) {  
  6.             this.info = info;  
  7.         }  
  8.         public String getInfo() {  
  9.             return info;  
  10.         }  
  11.     }  
  12.     private String userAgent; //用戶瀏覽器類型  
  13.     private OnlineStatus status = OnlineStatus.on_line; //在線狀態  
  14.     private String systemHost; //用戶登錄時系統IP  
  15.     //省略其他  
  16. }   

OnlineSession用於保存當前登錄用戶的在線狀態,支持如離線等狀態的控制。

 

接着自定義SessionFactory:

Java代碼  收藏代碼

  1. public class OnlineSessionFactory implements SessionFactory {  
  2.   
  3.     @Override  
  4.     public Session createSession(SessionContext initData) {  
  5.         OnlineSession session = new OnlineSession();  
  6.         if (initData != null && initData instanceof WebSessionContext) {  
  7.             WebSessionContext sessionContext = (WebSessionContext) initData;  
  8.             HttpServletRequest request = (HttpServletRequest) sessionContext.getServletRequest();  
  9.             if (request != null) {  
  10.                 session.setHost(IpUtils.getIpAddr(request));  
  11.                 session.setUserAgent(request.getHeader("User-Agent"));  
  12.                 session.setSystemHost(request.getLocalAddr() + ":" + request.getLocalPort());  
  13.             }  
  14.         }  
  15.         return session;  
  16.     }  
  17. }   

根據會話上下文創建相應的OnlineSession。

 

最後在shiro-web.ini配置文件中配置:

Java代碼  收藏代碼

  1. sessionFactory=org.apache.shiro.session.mgt.OnlineSessionFactory  
  2. sessionManager.sessionFactory=$sessionFactory  

      

更多請參考https://github.com/zhangkaitao/es/tree/master/web/src/main/java/org/apache/shiro中的相關代碼。

 

 

示例源代碼:https://github.com/zhangkaitao/shiro-example; 探討Spring/Shiro技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章