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的缓存共享管理。

完!

posted @ 2013-07-23 22:10  土豆的奥特之父  阅读(2474)  评论(0编辑  收藏  举报