• Shiro经过Redis管理会话实现集群(转载)


    原文:http://www.myexception.cn/software-architecture-design/1815507.html

    Shiro通过Redis管理会话实现集群

    写在前面

    1.在上一篇帖子 Shiro一些补充 中提到过Shiro可以使用Shiro自己的Session或者自定义的Session来代替HttpSession

    2.Redis/Jedis参考我写的 http://sgq0085.iteye.com/category/317384 一系列内容

    一. SessionDao

    配置在sessionManager中,可选项,如果不修改默认使用MemorySessionDAO,即在本机内存中操作。

    如果想通过Redis管理Session,从这里入手。只需要实现类似DAO接口的CRUD即可。

    经过1:最开始通过继承AbstractSessionDAO实现,发现doReadSession方法调用过于频繁,所以改为通过集成CachingSessionDAO来实现。

    注意,本地缓存通过EhCache实现,失效时间一定要远小于Redis失效时间,这样本地失效后,会访问Redis读取,并重新设置Redis上会话数据的过期时间。

    因为Jedis API KEY和Value相同,同为String或同为byte[]为了方便扩展下面的方法

    package com.gqshao.authentication.utils;
    
    import com.google.common.collect.Lists;
    import org.apache.commons.lang3.SerializationUtils;
    import org.apache.shiro.codec.Base64;
    import org.apache.shiro.session.Session;
    
    import java.io.Serializable;
    import java.util.Collection;
    import java.util.List;
    
    public class SerializeUtils extends SerializationUtils {
    
        public static String serializeToString(Serializable obj) {
            try {
                byte[] value = serialize(obj);
                return Base64.encodeToString(value);
            } catch (Exception e) {
                throw new RuntimeException("serialize session error", e);
            }
        }
    
        public static Session deserializeFromString(String base64) {
            try {
                byte[] objectData = Base64.decode(base64);
                return deserialize(objectData);
            } catch (Exception e) {
                throw new RuntimeException("deserialize session error", e);
            }
        }
    
        public static <T> Collection<T> deserializeFromStringController(Collection<String> base64s) {
            try {
                List<T> list = Lists.newLinkedList();
                for (String base64 : base64s) {
                    byte[] objectData = Base64.decode(base64);
                    T t = deserialize(objectData);
                    list.add(t);
                }
                return list;
            } catch (Exception e) {
                throw new RuntimeException("deserialize session error", e);
            }
        }
    }
    

    我的Dao实现,ShiroSession是我自己实现的,原因在后面说明,默认使用的是SimpleSession

    package com.gqshao.authentication.dao;
    
    import com.gqshao.authentication.session.ShiroSession;
    import com.gqshao.authentication.utils.SerializeUtils;
    import com.gqshao.redis.component.JedisUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.ValidatingSession;
    import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
    import org.apache.shiro.subject.support.DefaultSubjectContext;
    import org.apache.shiro.util.CollectionUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.Transaction;
    
    import java.io.Serializable;
    import java.util.Collection;
    import java.util.List;
    import java.util.Set;
    
    /**
     * 针对自定义的ShiroSession的Redis CRUD操作,通过isChanged标识符,确定是否需要调用Update方法
     * 通过配置securityManager在属性cacheManager查找从缓存中查找Session是否存在,如果找不到才调用下面方法
     * Shiro内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了CacheManagerAware并自动注入相应的CacheManager。
     */
    public class CachingShiroSessionDao extends CachingSessionDAO {
    
        private static final Logger logger = LoggerFactory.getLogger(CachingShiroSessionDao.class);
    
        // 保存到Redis中key的前缀 prefix+sessionId
        private String prefix = "";
    
        // 设置会话的过期时间
        private int seconds = 0;
    
        @Autowired
        private JedisUtils jedisUtils;
    
        /**
         * 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读
         */
        @Override
        public Session readSession(Serializable sessionId) throws UnknownSessionException {
            Session session = getCachedSession(sessionId);
            if (session == null
                    || session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
                session = this.doReadSession(sessionId);
                if (session == null) {
                    throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
                } else {
                    // 缓存
                    cache(session, session.getId());
                }
            }
            return session;
        }
    
        /**
         * 根据会话ID获取会话
         *
         * @param sessionId 会话ID
         * @return ShiroSession
         */
        @Override
        protected Session doReadSession(Serializable sessionId) {
            Session session = null;
            Jedis jedis = null;
            try {
                jedis = jedisUtils.getResource();
                String key = prefix + sessionId;
                String value = jedis.get(key);
                if (StringUtils.isNotBlank(value)) {
                    session = SerializeUtils.deserializeFromString(value);
                    logger.info("sessionId {} ttl {}: ", sessionId, jedis.ttl(key));
                    // 重置Redis中缓存过期时间
                    jedis.expire(key, seconds);
                    logger.info("sessionId {} name {} 被读取", sessionId, session.getClass().getName());
                }
            } catch (Exception e) {
                logger.warn("读取Session失败", e);
            } finally {
                jedisUtils.returnResource(jedis);
            }
    
            return session;
        }
    
        public Session doReadSessionWithoutExpire(Serializable sessionId) {
            Session session = null;
            Jedis jedis = null;
            try {
                jedis = jedisUtils.getResource();
                String key = prefix + sessionId;
                String value = jedis.get(key);
                if (StringUtils.isNotBlank(value)) {
                    session = SerializeUtils.deserializeFromString(value);
                }
            } catch (Exception e) {
                logger.warn("读取Session失败", e);
            } finally {
                jedisUtils.returnResource(jedis);
            }
    
            return session;
        }
    
        /**
         * 如DefaultSessionManager在创建完session后会调用该方法;
         * 如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;
         * 返回会话ID;主要此处返回的ID.equals(session.getId());
         */
        @Override
        protected Serializable doCreate(Session session) {
            // 创建一个Id并设置给Session
            Serializable sessionId = this.generateSessionId(session);
            assignSessionId(session, sessionId);
            Jedis jedis = null;
            try {
                jedis = jedisUtils.getResource();
                // session由Redis缓存失效决定,这里只是简单标识
                session.setTimeout(seconds);
                jedis.setex(prefix + sessionId, seconds, SerializeUtils.serializeToString((ShiroSession) session));
                logger.info("sessionId {} name {} 被创建", sessionId, session.getClass().getName());
            } catch (Exception e) {
                logger.warn("创建Session失败", e);
            } finally {
                jedisUtils.returnResource(jedis);
            }
            return sessionId;
        }
    
        /**
         * 更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
         */
        @Override
        protected void doUpdate(Session session) {
            //如果会话过期/停止 没必要再更新了
            try {
                if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                    return;
                }
            } catch (Exception e) {
                logger.error("ValidatingSession error");
            }
    
            Jedis jedis = null;
            try {
                if (session instanceof ShiroSession) {
                    // 如果没有主要字段(除lastAccessTime以外其他字段)发生改变
                    ShiroSession ss = (ShiroSession) session;
                    if (!ss.isChanged()) {
                        return;
                    }
                    Transaction tx = null;
                    try {
                        jedis = jedisUtils.getResource();
                        // 开启事务
                        tx = jedis.multi();
                        ss.setChanged(false);
                        tx.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString(ss));
                        logger.info("sessionId {} name {} 被更新", session.getId(), session.getClass().getName());
                        // 执行事务
                        tx.exec();
                    } catch (Exception e) {
                        if (tx != null) {
                            // 取消执行事务
                            tx.discard();
                        }
                        throw e;
                    }
    
                } else if (session instanceof Serializable) {
                    jedis = jedisUtils.getResource();
                    jedis.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString((Serializable) session));
                    logger.info("sessionId {} name {} 作为非ShiroSession对象被更新, ", session.getId(), session.getClass().getName());
                } else {
                    logger.warn("sessionId {} name {} 不能被序列化 更新失败", session.getId(), session.getClass().getName());
                }
            } catch (Exception e) {
                logger.warn("更新Session失败", e);
            } finally {
                jedisUtils.returnResource(jedis);
            }
        }
    
        /**
         * 删除会话;当会话过期/会话停止(如用户退出时)会调用
         */
        @Override
        protected void doDelete(Session session) {
            Jedis jedis = null;
            try {
                jedis = jedisUtils.getResource();
                jedis.del(prefix + session.getId());
                logger.debug("Session {} 被删除", session.getId());
            } catch (Exception e) {
                logger.warn("修改Session失败", e);
            } finally {
                jedisUtils.returnResource(jedis);
            }
        }
    
        /**
         * 删除cache中缓存的Session
         */
        public void uncache(Serializable sessionId) {
            Session session = this.readSession(sessionId);
            super.uncache(session);
            logger.info("取消session {} 的缓存", sessionId);
        }
    
        /**
         * 获取当前所有活跃用户,如果用户量多此方法影响性能
         */
        @Override
        public Collection<Session> getActiveSessions() {
            Jedis jedis = null;
            try {
                jedis = jedisUtils.getResource();
                Set<String> keys = jedis.keys(prefix + "*");
                if (CollectionUtils.isEmpty(keys)) {
                    return null;
                }
                List<String> valueList = jedis.mget(keys.toArray(new String[0]));
                return SerializeUtils.deserializeFromStringController(valueList);
            } catch (Exception e) {
                logger.warn("统计Session信息失败", e);
            } finally {
                jedisUtils.returnResource(jedis);
            }
            return null;
        }
    
        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }
    
        public void setSeconds(int seconds) {
            this.seconds = seconds;
        }
    
    }
    

    二.Session和SessionFactory

    步骤2:经过上面的开发已经可以使用的,但发现每次访问都会多次调用SessionDAO的doUpdate方法,来更新Redis上数据,过来发现更新的字段只有LastAccessTime(最后一次访问时间),由于会话失效是由Redis数据过期实现的,这个字段意义不大,为了减少对Redis的访问,降低网络压力,实现自己的Session,在SimpleSession上套一层,增加一个标识位,如果Session除lastAccessTime意外其它字段修改,就标识一下,只有标识为修改的才可以通过doUpdate访问Redis,否则直接返回。这也是上面SessionDao中doUpdate中逻辑判断的意义

    package com.gqshao.authentication.session;
    
    
    import org.apache.shiro.session.mgt.SimpleSession;
    
    import java.io.Serializable;
    import java.util.Date;
    import java.util.Map;
    
    
    /**
     * 由于SimpleSession lastAccessTime更改后也会调用SessionDao update方法,
     * 增加标识位,如果只是更新lastAccessTime SessionDao update方法直接返回
     */
    public class ShiroSession extends SimpleSession implements Serializable {
        // 除lastAccessTime以外其他字段发生改变时为true
        private boolean isChanged;
    
        public ShiroSession() {
            super();
            this.setChanged(true);
        }
    
        public ShiroSession(String host) {
            super(host);
            this.setChanged(true);
        }
    
    
        @Override
        public void setId(Serializable id) {
            super.setId(id);
            this.setChanged(true);
        }
    
        @Override
        public void setStopTimestamp(Date stopTimestamp) {
            super.setStopTimestamp(stopTimestamp);
            this.setChanged(true);
        }
    
        @Override
        public void setExpired(boolean expired) {
            super.setExpired(expired);
            this.setChanged(true);
        }
    
        @Override
        public void setTimeout(long timeout) {
            super.setTimeout(timeout);
            this.setChanged(true);
        }
    
        @Override
        public void setHost(String host) {
            super.setHost(host);
            this.setChanged(true);
        }
    
        @Override
        public void setAttributes(Map<Object, Object> attributes) {
            super.setAttributes(attributes);
            this.setChanged(true);
        }
    
        @Override
        public void setAttribute(Object key, Object value) {
            super.setAttribute(key, value);
            this.setChanged(true);
        }
    
        @Override
        public Object removeAttribute(Object key) {
            this.setChanged(true);
            return super.removeAttribute(key);
        }
    
        /**
         * 停止
         */
        @Override
        public void stop() {
            super.stop();
            this.setChanged(true);
        }
    
        /**
         * 设置过期
         */
        @Override
        protected void expire() {
            this.stop();
            this.setExpired(true);
        }
    
        public boolean isChanged() {
            return isChanged;
        }
    
        public void setChanged(boolean isChanged) {
            this.isChanged = isChanged;
        }
    
        @Override
        public boolean equals(Object obj) {
            return super.equals(obj);
        }
    
        @Override
        protected boolean onEquals(SimpleSession ss) {
            return super.onEquals(ss);
        }
    
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    
        @Override
        public String toString() {
            return super.toString();
        }
    }
    
    package com.gqshao.authentication.session;
    
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.mgt.SessionContext;
    import org.apache.shiro.session.mgt.SessionFactory;
    
    public class ShiroSessionFactory implements SessionFactory {
    
        @Override
        public Session createSession(SessionContext initData) {
            ShiroSession session = new ShiroSession();
            return session;
        }
    }
    

    三.SessionListener

    步骤3:发现用户推出后,Session没有从Redis中销毁,虽然当前重新new了一个,但会对统计带来干扰,通过SessionListener解决这个问题

    package com.gqshao.authentication.listener;
    
    import com.gqshao.authentication.dao.CachingShiroSessionDao;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.SessionListener;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    
    public class ShiroSessionListener implements SessionListener {
    
        private static final Logger logger = LoggerFactory.getLogger(ShiroSessionListener.class);
    
        @Autowired
        private CachingShiroSessionDao sessionDao;
    
        @Override
        public void onStart(Session session) {
            // 会话创建时触发
            logger.info("ShiroSessionListener session {} 被创建", session.getId());
        }
    
        @Override
        public void onStop(Session session) {
            sessionDao.delete(session);
            // 会话被停止时触发
            logger.info("ShiroSessionListener session {} 被销毁", session.getId());
        }
    
        @Override
        public void onExpiration(Session session) {
            sessionDao.delete(session);
            //会话过期时触发
            logger.info("ShiroSessionListener session {} 过期", session.getId());
        }
    }
    

    四.将账号信息放到Session中

    修改realm中AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)方法,在返回AuthenticationInfo之前添加下面的代码,把用户信息放到Session中

    // 把账号信息放到Session中,并更新缓存,用于会话管理
    Subject subject = SecurityUtils.getSubject();
    Serializable sessionId = subject.getSession().getId();
    ShiroSession session = (ShiroSession) sessionDao.doReadSessionWithoutExpire(sessionId);
    session.setAttribute("userId", su.getId());
    session.setAttribute("loginName", su.getLoginName());
    sessionDao.update(session);

      

    五. 配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:util="http://www.springframework.org/schema/util"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
    	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd">
    
    
        <description>Shiro安全配置</description>
    
        <!-- Shiro's main business-tier object for web-enabled applications -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <property name="realm" ref="shiroDbRealm"/>
            <!-- 可选项 最好使用,SessionDao,中 doReadSession 读取过于频繁了-->
            <property name="cacheManager" ref="shiroEhcacheManager"/>
            <!--可选项 默认使用ServletContainerSessionManager,直接使用容器的HttpSession,可以通过配置sessionManager,使用DefaultWebSessionManager来替代-->
            <property name="sessionManager" ref="sessionManager"/>
        </bean>
    
        <!-- 項目自定义的Realm -->
        <bean id="shiroDbRealm" class="com.gqshao.authentication.realm.ShiroDbRealm"/>
    
        <!-- Shiro Filter -->
        <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
            <property name="securityManager" ref="securityManager"/>
            <!-- 指向登陆路径,整合spring时指向控制器方法地址 -->
            <property name="loginUrl" value="/login"/>
            <property name="successUrl" value="/"/>
            <!-- 可选配置,通过实现自己的AuthenticatingFilter实现表单的自定义 -->
            <property name="filters">
                <util:map>
                    <entry key="authc">
                        <bean class="com.gqshao.authentication.filter.MyAuthenticationFilter"/>
                    </entry>
                </util:map>
            </property>
    
            <property name="filterChainDefinitions">
                <value>
                    /login = authc
                    /logout = logout
                    /static/** = anon
                    /** = user
                </value>
            </property>
        </bean>
    
        <!-- 用户授权信息Cache, 采用EhCache,本地缓存最长时间应比中央缓存时间短一些,以确保Session中doReadSession方法调用时更新中央缓存过期时间 -->
        <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
            <property name="cacheManagerConfigFile" value="classpath:security/ehcache-shiro.xml"/>
        </bean>
    
        <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
            <!-- 设置全局会话超时时间,默认30分钟(1800000) -->
            <property name="globalSessionTimeout" value="1800000"/>
            <!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true-->
            <property name="deleteInvalidSessions" value="false"/>
            <!-- 是否开启会话验证器任务 默认true -->
            <property name="sessionValidationSchedulerEnabled" value="false"/>
            <!-- 会话验证器调度时间 -->
            <property name="sessionValidationInterval" value="1800000"/>
            <property name="sessionFactory" ref="sessionFactory"/>
            <property name="sessionDAO" ref="sessionDao"/>
            <!-- 默认JSESSIONID,同tomcat/jetty在cookie中缓存标识相同,修改用于防止访问404页面时,容器生成的标识把shiro的覆盖掉 -->
            <property name="sessionIdCookie">
                <bean class="org.apache.shiro.web.servlet.SimpleCookie">
                    <constructor-arg name="name" value="SHRIOSESSIONID"/>
                </bean>
            </property>
            <property name="sessionListeners">
                <list>
                    <bean class="com.gqshao.authentication.listener.ShiroSessionListener"/>
                </list>
            </property>
        </bean>
    
        <!-- 自定义Session工厂方法 返回会标识是否修改主要字段的自定义Session-->
        <bean id="sessionFactory" class="com.gqshao.authentication.session.ShiroSessionFactory"/>
    
        <!-- 普通持久化接口,不会被缓存 每次doReadSession会被反复调用 -->
        <!--<bean class="com.gqshao.authentication.dao.RedisSessionDao">-->
        <!-- 使用可被缓存的Dao ,本地缓存减轻网络压力 -->
        <!--<bean id="sessionDao" class="com.gqshao.authentication.dao.CachingSessionDao">-->
        <!-- 可缓存Dao,操作自定义Session,添加标识位,减少doUpdate方法中Redis的连接次数来减轻网络压力 -->
        <bean id="sessionDao" class="com.gqshao.authentication.dao.CachingShiroSessionDao">
            <property name="prefix" value="ShiroSession_"/>
            <!-- 注意中央缓存有效时间要比本地缓存有效时间长-->
            <property name="seconds" value="1800"/>
        </bean>
    
    
        <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
        <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    
        <!-- AOP式方法级权限检查 -->
        <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
              depends-on="lifecycleBeanPostProcessor">
            <property name="proxyTargetClass" value="true"/>
        </bean>
        <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
            <property name="securityManager" ref="securityManager"/>
        </bean>
    </beans>
    <ehcache updateCheck="false" name="shiroCache">
        <!--
            timeToIdleSeconds 当缓存闲置n秒后销毁 为了保障会调用ShiroSessionDao的doReadSession方法,所以不配置改属性
            timeToLiveSeconds 当缓存存活n秒后销毁 必须必Redis中过期时间短
        -->
        <defaultCache
                maxElementsInMemory="10000"
                eternal="false"
                timeToLiveSeconds="60"
                overflowToDisk="false"
                diskPersistent="false"
                diskExpiryThreadIntervalSeconds="10"
                />
    </ehcache>

    六.测试会话管理

    package com.gqshao.authentication.controller;
    
    import com.gqshao.authentication.dao.CachingShiroSessionDao;
    import org.apache.shiro.session.Session;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.io.Serializable;
    import java.util.Collection;
    
    @Controller
    @RequestMapping("/session")
    public class SessionController {
    
        @Autowired
        private CachingShiroSessionDao sessionDao;
    
        @RequestMapping("/active")
        @ResponseBody
        public Collection<Session> getActiveSessions() {
            return sessionDao.getActiveSessions();
        }
    
        @RequestMapping("/read")
        @ResponseBody
        public Session readSession(Serializable sessionId) {
            return sessionDao.doReadSessionWithoutExpire(sessionId);
        }
    }
    

     七.集群情况下的改造

    1.问题上面启用了Redis中央缓存、EhCache本地JVM缓存,AuthorizingRealm的doGetAuthenticationInfo登陆认证方法返回的AuthenticationInfo,默认情况下会被保存到Session的Attribute下面两个字段中

    org.apache.shiro.subject.support.DefaultSubjectContext.PRINCIPALS_SESSION_KEY 保存 principal
    org.apache.shiro.subject.support.DefaultSubjectContext.AUTHENTICATED_SESSION_KEY 保存 boolean是否登陆

    然后在每次请求过程中,在ShiroFilter中组装Subject时,读取Session中这两个字段

    现在的问题是Session被缓存到本地JVM堆中,也就是说服务器A登陆,无法修改服务器B的EhCache中Session属性,导致服务器B没有登陆。

    处理方法有很多思路,比如重写CachingSessionDAO,readSession如果没有这两个属性就不缓存(没登陆就不缓存),或者cache的session没有这两个属性就调用自己实现的doReadSession方法从Redis中重读一下。

    /**
     * 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读
     */
    @Override
    public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session session = getCachedSession(sessionId);
        if (session == null
                || session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
            session = this.doReadSession(sessionId);
            if (session == null) {
                throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
            } else {
                // 缓存
                cache(session, session.getId());
            }
        }
        return session;
    }

    2.如果需要保持各个服务器Session是完全同步的,可以通过Redis消息订阅/发布功能,再调用SessionDao中实现了删除Session本地缓存的方法

  • 相关阅读:
    jquery图片播放弹出插件Fancybox
    D3js-API介绍【英】
    ZOJ 3156 Taxi (二分匹配+二分查找)
    linux权限之su和sudo的差别
    CareerCup之1.6 Rotate Image
    [oracle]pl/sql --分页过程demo
    已迁移到http://www.coffin5257.com
    C# 之 集合ArrayList
    Java 序列化Serializable具体解释(附具体样例)
    Android 短信验证码控件
  • 原文地址:https://www.cnblogs.com/shihaiming/p/6406640.html
Copyright © 2020-2023  润新知