`

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

阅读更多

流程概要说明

1.Servlet容器在用户浏览器首次访问后会产生Session,并将Session的ID保存到Cookie中(浏览器不同key不一定相同),同时Shiro会将该Session缓存到Redis中; 

 

2.用户登录认证成功后Shiro会修改Session属性,添加用户认证成功标识,并同步修改Redis中Session;

 

3.用户发起请求后,Shiro会先判断本地EhCache缓存中是否存在该Session,如果有,直接从本地EhCache缓存中读取,如果没有再从Redis中读取Session,并在此时判断Session是否认证通过,如果认证通过将该Session缓存到本地EhCache中; 

 

4.如果Session发生改变,或被删除(用户退出登录),先对Redis中Session做相应修改(修改或删除);再通过Redis消息通道发布缓存失效消息,通知其它节点EhCache失效。

1.S

写在前面

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[]为了方便扩展下面的方法

 

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 <T> T 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

import com.genertech.adp.web.common.utils.SerializeUtils;
import com.genertech.adp.web.sys.authentication.component.ShiroSession;
import com.genertech.adp.web.sys.redis.component.JedisUtils;
import org.apache.commons.lang3.StringUtils;
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.joda.time.DateTime;
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 ShiroSessionDao extends CachingSessionDAO {

    private static final Logger logger = LoggerFactory.getLogger(ShiroSessionDao.class);

    // 保存到Redis中key的前缀 prefix+sessionId
    private String prefix = "";

    // 设置会话的过期时间
    private int seconds = 0;

    // 特殊配置 只用于没有Redis时 将Session放到EhCache中
    private Boolean onlyEhCache;

    @Autowired
    private JedisUtils jedisUtils;

    /**
     * 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读
     */
//    @Override
    public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session cached = null;
        try {
            cached = super.getCachedSession(sessionId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (onlyEhCache) {
            return cached;
        }
        // 如果缓存不存在或者缓存中没有登陆认证后记录的信息就重新从Redis中读取
        if (cached == null || cached.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
            try {
                cached = this.doReadSession(sessionId);
                if (cached == null) {
                    throw new UnknownSessionException();
                } else {
                    // 重置Redis中缓存过期时间并缓存起来 只有设置change才能更改最后一次访问时间
                    ((ShiroSession) cached).setChanged(true);
                    super.update(cached);
                }
            } catch (Exception e) {
                logger.warn("There is no session with id [" + sessionId + "]");
            }
        }
        return cached;
    }

    /**
     * 从Redis中读取Session,并重置过期时间
     *
     * @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("shiro session id {} 被读取", sessionId);
            }
        } catch (Exception e) {
            logger.warn("读取Session失败", e);
        } finally {
            jedisUtils.returnResource(jedis);
        }

        return session;
    }


    /**
     * 从Redis中读取,但不重置Redis中缓存过期时间
     */
    public Session doReadSessionWithoutExpire(Serializable sessionId) {
        if (onlyEhCache) {
            return readSession(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);
        if (onlyEhCache) {
            return sessionId;
        }
        Jedis jedis = null;
        try {
            jedis = jedisUtils.getResource();
            // session由Redis缓存失效决定,这里只是简单标识
            session.setTimeout(seconds);
            jedis.setex(prefix + sessionId, seconds, SerializeUtils.serializeToString((ShiroSession) session));
            logger.info("shiro session id {} 被创建", sessionId);
        } 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");
        }

        if (onlyEhCache) {
            return;
        }

        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);
                    ss.setLastAccessTime(DateTime.now().toDate());
                    tx.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString(ss));
                    logger.info("shiro session id {} 被更新", 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("ID {} classname {} 作为非ShiroSession对象被更新, ", session.getId(), session.getClass().getName());
            } else {
                logger.info("ID {} classname {} 不能被序列化 更新失败", session.getId(), session.getClass().getName());
            }
        } catch (Exception e) {
            logger.warn("更新Session失败", e);
        } finally {
            jedisUtils.returnResource(jedis);
        }
    }

    /**
     * 删除会话;当会话过期/会话停止(如用户退出时)会调用
     */
    @Override
    public void doDelete(Session session) {
        Jedis jedis = null;
        try {
            jedis = jedisUtils.getResource();
            jedis.del(prefix + session.getId());
            logger.info("shiro session id {} 被删除", session.getId());
        } catch (Exception e) {
            logger.warn("删除Session失败", e);
        } finally {
            jedisUtils.returnResource(jedis);
        }
    }

    /**
     * 删除cache中缓存的Session
     */
    public void uncache(Serializable sessionId) {
        try {
            Session session = super.getCachedSession(sessionId);
            super.uncache(session);
            logger.info("shiro session id {} 的缓存失效", sessionId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取当前所有活跃用户,如果用户量多此方法影响性能
     */
    @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;
    }

    /**
     * 返回本机Ehcache中Session
     */
    public Collection<Session> getEhCacheActiveSessions() {
        return super.getActiveSessions();
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public void setSeconds(int seconds) {
        this.seconds = seconds;
    }

    public void setOnlyEhCache(Boolean onlyEhCache) {
        this.onlyEhCache = onlyEhCache;
    }
}

 

 

 

 

二.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.commons.lang3.StringUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.session.mgt.SessionFactory;
import org.apache.shiro.web.session.mgt.DefaultWebSessionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

public class ShiroSessionFactory implements SessionFactory {
    private static final Logger logger = LoggerFactory.getLogger(ShiroSessionFactory.class);

    @Override
    public Session createSession(SessionContext initData) {
        ShiroSession session = new ShiroSession();
        HttpServletRequest request = (HttpServletRequest)initData.get(DefaultWebSessionContext.class.getName() + ".SERVLET_REQUEST");
        session.setHost(getIpAddress(request));
        return session;
    }

    public static String getIpAddress(HttpServletRequest request) {
        String localIP = "127.0.0.1";
        String ip = request.getHeader("x-forwarded-for");
        if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

 

 

三.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"/>
        <!-- 特殊配置 只用于没有Redis时 将Session放到EhCache中 -->
        <property name="onlyEhCache" value="false"/>
    </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中重读一下。

 

2.readSession中每次调用doReadSession方法的时候,都代表第一次读取,或本地EhCache失效,我们可以在这个时候调用一下updateSession方法,重新设置一下最后一次访问时间,当然要把isChange设置为true才会保存到Redis中。

 

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

 

 

 

 

分享到:
评论
14 楼 sgq0085 2017-10-18  
无尘灬 写道
楼主,在吗?可以加你qq咨询一下问题吗?

公司禁用QQ。。。
13 楼 无尘灬 2017-09-20  
楼主,在吗?可以加你qq咨询一下问题吗?
12 楼 zhouminsen 2017-02-20  
感谢楼主的无私奉献
11 楼 asdhobby 2016-09-20  
楼主,个人感觉每次调用SessionDAO的doUpdate方法时都会更新Redis的过期时间,不只是更新LastAccessTime,在拦截时通过LastAccessTime字段判断会有问题
10 楼 597272095 2016-08-19  
请问 能用mongodb替换Redis来实现session集群吗
9 楼 qiulongjie0112 2016-07-27  
楼主,求一份完整的shiro,redis代码
8 楼 ydqonh 2016-06-14  
sgq0085 写道
筱龙缘 写道
sgq0085 写道
筱龙缘 写道
楼主 照你的配置之后 为什么第一次访问会很慢呢

有多慢?

5s以上

不应该啊 我们项目都上了 没出现这个问题

是不是未登录情况下仍多次访问redis的原因,博主,看了你的文章,收获很大,现在仍有些小问题,就是第一次打开时比较慢,我有个问题请教下,既然redis的globalSessionTimeout是否可以设置为永不过期,只依赖ehcache和redis的过期,这样只需判断本地cache是否为空即可,不需要判断用户是否过期,不知道这样是否可行,有没有什么隐患,谢谢~
7 楼 sgq0085 2015-07-18  
筱龙缘 写道
sgq0085 写道
筱龙缘 写道
楼主 照你的配置之后 为什么第一次访问会很慢呢

有多慢?

5s以上

不应该啊 我们项目都上了 没出现这个问题
6 楼 筱龙缘 2015-07-18  
sgq0085 写道
筱龙缘 写道
楼主 照你的配置之后 为什么第一次访问会很慢呢

有多慢?

5s以上
5 楼 sgq0085 2015-07-06  
筱龙缘 写道
楼主 照你的配置之后 为什么第一次访问会很慢呢

有多慢?
4 楼 筱龙缘 2015-07-04  
楼主 照你的配置之后 为什么第一次访问会很慢呢
3 楼 sgq0085 2015-06-26  
筱龙缘 写道
楼主 代码能打包 或者在git上么?

在github上啊,https://github.com/sgq0085/learn
2 楼 筱龙缘 2015-06-25  
楼主 代码能打包 或者在git上么?
1 楼 lsy 2015-06-22  
doReadSessionWithoutExpire和doReadSession代码实现和注释怎么感觉不对称呢?

相关推荐

Global site tag (gtag.js) - Google Analytics