• 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集群配置-->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="globalSessionTimeout" value="3600000"/>
        <property name="sessionDAO" ref="zkShiroSessionDAO"/>
        <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <property name="sessionIdCookie" ref="wapsession"/>
    </bean>
    
    <!--
    指定本系统SESSIONID, 默认为: JSESSIONID
    问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
    当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失!
    -->
    <bean id="wapsession" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg name="name" value="WAPSESSIONID"/>
    </bean>
    
    <!--
    定时清理僵尸session,Shiro会启用一个后台守护线程定时执行清理操作
    用户直接关闭浏览器造成的孤立会话
    -->
    <bean id="sessionValidationScheduler"
          class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
        <property name="interval" value="3600000"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>
    
    <!--由zk做session存储容器-->
    <bean id="zkShiroSessionDAO" class="b2gonline.incometaxexamine._systembase.shiro.ZKShiroSessionDAO">
        <!--使用内存缓存登录用户信息,一次获取用户登录信息后缓存到内存减少Shiro大量的读取操作,用户退出或超时后自动清除-->
        <constructor-arg name="useMemCache" value="true"/>    
        <property name="zookeeperTemplate" ref="zookeeperTemplate"/>
        <property name="shiroSessionZKPath" value="/SHIROSESSIONS"/>
        <property name="sessionPrefix" value="session-"/>
    </bean>
    
    <!-- SHIRO安全接口 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        ...
        <property name="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
     */
    public class ZKShiroSessionDAO extends CachingSessionDAO {
    
        public ZKShiroSessionDAO() {
        }
    
        private boolean useMemCache = false;
    
        /**
         * SESSION ZK DAO 实例
         * 如果开户缓存
         * 用户登录时自动缓存, 用户登录超时自动删除
         * 由于shiro的cacheManager是全局的, 所以这里使用setActiveSessionsCache直接设置Cache来本地缓存, 而不使用全局zk缓存.
         * 由于同一用户可能会被路由到不同服务器,所以在doReadSession方法里也做了缓存增加.
         *
         * @param useMemCache 是否使用内存缓存登录信息
         */
        public ZKShiroSessionDAO(boolean useMemCache) {
            this.useMemCache = useMemCache;
            if (useMemCache) {
                setActiveSessionsCache(
                        new MapCache<>(this.ACTIVE_SESSION_CACHE_NAME, new ConcurrentHashMap<Serializable, Session>())
                );
            }
        }
    
        Logger logger = LoggerFactory.getLogger(this.getClass());
    
        /**
         * ZK操作类
         */
        private IZookeeperTemplate zookeeperTemplate;
    
        /**
         * 缓存根路径, 结尾不加/
         */
        private String shiroSessionZKPath = "/SHIROSESSIONS";
    
        /**
         * 缓存项前缀
         */
        private String sessionPrefix = "session-";
    
        /**
         * 设置Shiro Session 前缀 默认 session-
         *
         * @param sessionPrefix
         */
        public void setSessionPrefix(String sessionPrefix) {
            this.sessionPrefix = sessionPrefix;
        }
    
    
        public void setZookeeperTemplate(IZookeeperTemplate zookeeperTemplate) {
            this.zookeeperTemplate = zookeeperTemplate;
        }
    
        /**
         * 设置Shiro在ZK服务器存放根路径
         *
         * @param shiroSessionZKPath 默认值:/SHIROSESSIONS/
         */
        public void setShiroSessionZKPath(String shiroSessionZKPath) {
            this.shiroSessionZKPath = shiroSessionZKPath;
        }
    
        /**
         * session更新
         *
         * @param session
         * @throws UnknownSessionException
         */
        @Override
        public void update(Session session) throws UnknownSessionException {
            if (session == null || session.getId() == null) {
                logger.error("session argument cannot be null.");
            }
            saveSession(session, "update");
        }
    
        @Override
        protected void doUpdate(Session session) {
        }
    
        /**
         * session删除
         *
         * @param session
         */
        @Override
        public void delete(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);
            }
        }
    
        @Override
        protected void doDelete(Session session) {
        }
    
        /**
         * 获取当前活跃的session, 当前在线数量
         *
         * @return
         */
        @Override
        public Collection<Session> getActiveSessions() {
            ZNode zNode = new ZNode();
            zNode.setPath(shiroSessionZKPath);
            Set<Session> sessions = new HashSet<Session>();
            //读取所有SessionID  , 返回形如: session-9e3b5707-fa80-4d32-a6c9-f1c3685263a5
            List<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
         */
        @Override
        protected Serializable doCreate(Session session) {
            Serializable sessionId = this.generateSessionId(session);
            this.assignSessionId(session, sessionId);
            saveSession(session, "create");
            return sessionId;
        }
    
        /**
         * session读取
         *
         * @param id
         * @return
         */
        @Override
        protected Session doReadSession(Serializable id) {
            if (id == null) {
                logger.error("id is null!");
                return null;
            }
            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 {
                return null;
            }
        }
    
        /**
         * 生成全路径
         *
         * @param sessID
         * @return
         */
        private String getPath(Serializable sessID) {
            return shiroSessionZKPath + '/' + sessionPrefix + sessID.toString();
        }
    
        /**
         * session读取或更新
         *
         * @param session
         * @param act     update/save
         */
        private void saveSession(Session session, String act) {
            Serializable sessionId = session.getId();
            ZNode sessionNode = new ZNode();
            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定时清理!

    下一篇,有关Shiro的缓存共享管理。

    完!

  • 相关阅读:
    VS2010工具箱中的控件突然全部都不见了的问题解决
    wpf用户控件 弹出窗口因主窗体最小化而消失的问题
    未解析成员Csla.Security.UnauthenticatedPrincipal,Csla,异常
    服务器被入侵,管理员账号密码被改,策略以及维护
    telerik for asp 的引用问题
    decimal简单问题
    vs2008的网站升级为vs2010遇到的问题
    VS2010设置默认以管理员权限打开
    lhgdialog基础
    SQL Server 2005镜像删除备忘录
  • 原文地址:https://www.cnblogs.com/xguo/p/3209529.html
Copyright © 2020-2023  润新知