1. 程式人生 > >Apache-Shiro+Zookeeper系統叢集安全解決方案之會話管理

Apache-Shiro+Zookeeper系統叢集安全解決方案之會話管理

如今的系統多不是孤軍奮戰,在多結點會話共享管理方面有著各自的解決辦法,比如Session粘連,基於Web容器的各種處理等或者類似本文說的完全接管Web容器的Session管理,只是做法不盡相同。

而本文說的是Apache-Shiro+Zookeeper來解決多結點會話管理,Shiro一個優秀的許可權框架,有著很好的擴充套件性,而Zookeeper更是讓你激動不已的多功能分散式協調系統,在本例中就用它來做Shiro的會話持久容器!

在有過Shiro和Zookeeper開發後這一切都非常容易理解,實現過程如下:

用到的框架技術:

Spring + Shiro + Zookeeper

第一步:配置WEB.XML

<filter><filter-name>shiroFilter</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class><init-param><param-name>targetFilterLifecycle</param-name><param-value>true</param-value></init-param></filter>
<filter-mapping><filter-name>shiroFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>

第二步:SHIRO整合SPRING配置

applicationContext-shiro.xml 虛擬碼:

<!--Session叢集配置--><beanid="sessionManager"class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"
><propertyname="globalSessionTimeout"value="3600000"/><propertyname="sessionDAO"ref="zkShiroSessionDAO"/><propertyname="sessionValidationScheduler"ref="sessionValidationScheduler"/><propertyname="sessionValidationSchedulerEnabled"value="true"/><propertyname="sessionIdCookie"ref="wapsession"/></bean><!-- 指定本系統SESSIONID, 預設為: JSESSIONID 問題: 與SERVLET容器名衝突, 如JETTY, TOMCAT 等預設JSESSIONID, 當跳出SHIRO SERVLET時如ERROR-PAGE容器會為JSESSIONID重新分配值導致登入會話丟失! --><beanid="wapsession"class="org.apache.shiro.web.servlet.SimpleCookie"><constructor-argname="name"value="WAPSESSIONID"/></bean><!-- 定時清理殭屍session,Shiro會啟用一個後臺守護執行緒定時執行清理操作 使用者直接關閉瀏覽器造成的孤立會話 --><beanid="sessionValidationScheduler"class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler"><propertyname="interval"value="3600000"/><propertyname="sessionManager"ref="sessionManager"/></bean><!--由zk做session儲存容器--><beanid="zkShiroSessionDAO"class="b2gonline.incometaxexamine._systembase.shiro.ZKShiroSessionDAO"><!--使用記憶體快取登入使用者資訊,一次獲取使用者登入資訊後快取到記憶體減少Shiro大量的讀取操作,使用者退出或超時後自動清除--><constructor-argname="useMemCache"value="true"/><propertyname="zookeeperTemplate"ref="zookeeperTemplate"/><propertyname="shiroSessionZKPath"value="/SHIROSESSIONS"/><propertyname="sessionPrefix"value="session-"/></bean><!-- SHIRO安全介面 --><beanid="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> ... <propertyname="sessionManager"ref="sessionManager"/></bean>

第三步:Zookeeper對Shiro-SessionDao實現類

ZKShiroSessionDAO.JAVA虛擬碼:

import bgonline.foundation.hadoop.zk.IZookeeperTemplate;import bgonline.foundation.hadoop.zk.ZNode;import org.apache.shiro.cache.AbstractCacheManager;import org.apache.shiro.cache.Cache;import org.apache.shiro.cache.CacheException;import org.apache.shiro.cache.MapCache;import org.apache.shiro.session.Session;import org.apache.shiro.session.UnknownSessionException;import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;import org.apache.shiro.session.mgt.eis.CachingSessionDAO;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.util.SerializationUtils;import java.io.Serializable;import java.util.Collection;import java.util.HashSet;import java.util.List;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;/**
 * ZOOKEEPER實現SHIRO叢集SESSION儲存
 *
 * @author aliencode
 * @date 13-7-10
 */publicclassZKShiroSessionDAOextendsCachingSessionDAO{publicZKShiroSessionDAO(){}privateboolean useMemCache =false;/**
     * SESSION ZK DAO 例項
     * 如果開戶快取
     * 使用者登入時自動快取, 使用者登入超時自動刪除
     * 由於shiro的cacheManager是全域性的, 所以這裡使用setActiveSessionsCache直接設定Cache來本地快取, 而不使用全域性zk快取.
     * 由於同一使用者可能會被路由到不同伺服器,所以在doReadSession方法裡也做了快取增加.
     *
     * @param useMemCache 是否使用記憶體快取登入資訊
     */publicZKShiroSessionDAO(boolean useMemCache){this.useMemCache = useMemCache;if(useMemCache){
            setActiveSessionsCache(newMapCache<>(this.ACTIVE_SESSION_CACHE_NAME,newConcurrentHashMap<Serializable,Session>()));}}Logger logger =LoggerFactory.getLogger(this.getClass());/**
     * ZK操作類
     */privateIZookeeperTemplate zookeeperTemplate;/**
     * 快取根路徑, 結尾不加/
     */privateString shiroSessionZKPath ="/SHIROSESSIONS";/**
     * 快取項字首
     */privateString sessionPrefix ="session-";/**
     * 設定Shiro Session 字首 預設 session-
     *
     * @param sessionPrefix
     */publicvoid setSessionPrefix(String sessionPrefix){this.sessionPrefix = sessionPrefix;}publicvoid setZookeeperTemplate(IZookeeperTemplate zookeeperTemplate){this.zookeeperTemplate = zookeeperTemplate;}/**
     * 設定Shiro在ZK伺服器存放根路徑
     *
     * @param shiroSessionZKPath 預設值:/SHIROSESSIONS/
     */publicvoid setShiroSessionZKPath(String shiroSessionZKPath){this.shiroSessionZKPath = shiroSessionZKPath;}/**
     * session更新
     *
     * @param session
     * @throws UnknownSessionException
     */@Overridepublicvoid update(Session session)throwsUnknownSessionException{if(session ==null|| session.getId()==null){
            logger.error("session argument cannot be null.");}
        saveSession(session,"update");}@Overrideprotectedvoid doUpdate(Session session){}/**
     * session刪除
     *
     * @param session
     */@Overridepublicvoiddelete(Session session){if(session ==null|| session.getId()==null){
            logger.error("session argument cannot be null.");}
        logger.debug("delete session for id: {}", session.getId());
        zookeeperTemplate.deleteNode(getPath(session.getId()));if(useMemCache){this.uncache(session);}}@Overrideprotectedvoid doDelete(Session session){}/**
     * 獲取當前活躍的session, 當前線上數量
     *
     * @return
     */@OverridepublicCollection<Session> getActiveSessions(){ZNode zNode =newZNode();
        zNode.setPath(shiroSessionZKPath);Set<Session> sessions =newHashSet<Session>();//讀取所有SessionID  , 返回形如: session-9e3b5707-fa80-4d32-a6c9-f1c3685263a5List<String> ss = zookeeperTemplate.getChildren(zNode);for(String id : ss){if(id.startsWith(sessionPrefix)){String noPrefixId = id.replace(sessionPrefix,"");Session session = doReadSession(noPrefixId);if(session !=null) sessions.add(session);}}
        logger.debug("shiro getActiveSessions. size: {}", sessions.size());return sessions;}/**
     * 建立session, 使用者登入
     *
     * @param session
     * @return
     */@OverrideprotectedSerializable doCreate(Session session){Serializable sessionId =this.generateSessionId(session);this.assignSessionId(session, sessionId);
        saveSession(session,"create");return sessionId;}/**
     * session讀取
     *
     * @param id
     * @return
     */@OverrideprotectedSession doReadSession(Serializable id){if(id ==null){
            logger.error("id is null!");returnnull;}
        logger.debug("doReadSession for path: {}", getPath(id));Session session;byte[] byteData = zookeeperTemplate.getData(getPath(id)).getByteData();if(byteData !=null&& byteData.length >0){
            session =(Session)SerializationUtils.deserialize(byteData);if(useMemCache){this.cache(session, id);
                logger.debug("doReadSession for path: {}, add cached !", getPath(id));}return session;}else{returnnull;}}/**
     * 生成全路徑
     *
     * @param sessID
     * @return
     */privateString getPath(Serializable sessID){return shiroSessionZKPath +'/'+ sessionPrefix + sessID.toString();}/**
     * session讀取或更新
     *
     * @param session
     * @param act     update/save
     */privatevoid saveSession(Session session,String act){Serializable sessionId = session.getId();ZNode sessionNode =newZNode();
        sessionNode.setByteData(SerializationUtils.serialize(session));
        sessionNode.setPath(getPath(sessionId));
        logger.debug("save session for id: {}, act: {}", sessionId, act);if(act =="update")
            zookeeperTemplate.setData(sessionNode);else
            zookeeperTemplate.createNode(sessionNode);}}

小結

本文主要給出會話管理的實現過程和部分核心程式碼,並且說到並解決了在使用Shiro開發時會遇到的幾個關鍵問題和心得,如

Shiro預設的JSESSIONID和WEB容器同名衝突,這個如果使用預設開發時當訪問404等錯誤頁面由WEb容器直接處理並由生成新的JSESSIONID使得Shiro退出;

SESSION會話快取,這個借鑑EnterpriseCacheSessionDAO,由於Shiro在訪問每個連結時都會讀取一次Session,所以在使用者成功登入後把Session儲存並快取到記憶體或本地以減少大量讀取操作;

孤立會話的清除,當用戶直接關閉瀏覽器會有Session孤立於儲存容器中,配置ExecutorServiceSessionValidationScheduler定時清理!