第十三节 Shiro集成Redis实现分布式集群Session共享

x33g5p2x  于2021-12-18 转载在 其他  
字(8.2k)|赞(0)|评价(0)|浏览(354)

一、使用Redis共享Session原理

所有服务器的session信息都存储到了同一个Redis集群中,即所有的服务都将 Session 的信息存储到 Redis 集群中,无论是对 Session 的注销、更新都会同步到集群中,达到了 Session 共享的目的。

     Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。

在实际工作中我们建议使用外部的缓存设备(包括Redis)来共享 Session,避免单个服务器节点挂掉而影响服务,共享数据都会放到外部缓存容器中。
 

二、SessionManager的配置

重要提示:

      Shiro的Sessin实现是SimpleSession。SimpleSession的所有的属性是transient,所以一般情况下,将其存放到Redis中会丢失所有的属性值。为了解决这种问题,必须要使用JDK序列化策略来序列化SimpleSession,因此这就是为什么我们把SimpleSession转成字节存储的原因。所以说,RedisTemplate的value的序列化策略必须要使用JdkSerializationRedisSerializer。

      为了能够让多个服务器共享Session,我们需要把Session存储到外部的缓存设备。

     我们需要让session在集群中共享,就需要替换Shiro默认的sessionManager。我们需要使用DefaultWebSessionManager作为SessionManager。

<!-- securityManager 对象-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        ...
        <!-- 引入sessionManager-->
        <property name="sessionManager" ref="sessionManager"/>
    </bean>

    <!-- 会话管理器 ,时间单位是毫秒-->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!--去掉URL中的JSESSIONID-->
        <property name="sessionIdUrlRewritingEnabled" value="false"/>
        <!-- 会话存活时间(毫秒),最好与Redis中缓存时间一致 -->
        <property name="globalSessionTimeout" value="600000"/><!-- 10分钟 -->
        <!-- 是否删除无效的session-->
        <property name="deleteInvalidSessions" value="true"/>
        <!-- 扫描session线程,负责清理超时会话 -->
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <!-- 使用的是QuartZ组件来定时清理-->
        <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
        <!-- session需要使用会话cookie模版-->
        <property name="sessionIdCookieEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
        <!-- 对session进行增删错改查的实现类
             ,如果不自己注入sessionDAO,defaultWebSessionManager会使用MemorySessionDAO用内存做为默认实现类-->
        <!-- https://blog.csdn.net/lishehe/article/details/45223823 -->
        <property name="sessionDAO" ref="sessionDAO"/>
    </bean>

    <!-- 会话验证调度器 ,时间单位是毫秒-->
    <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.
                             quartz.QuartzSessionValidationScheduler">
        <property name="sessionValidationInterval" value="30000"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>

    <!-- 会话 ID 生成器 -->
    <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>

    <!-- 自定义session会话存储的实现类 ,使用Redis来存储共享session,达到分布式部署目的-->
    <bean id="sessionDAO" class="com.jay.shiro.RedisSessionDao">
        <property name="redisService" ref="redisService"/>
        <!-- Session的过期时间,单位秒。session将存储在Redis集群中实现共享-->
        <!-- Redis设置半小时的缓存失效时间 -->
        <property name="expireSeconds" value="600000"/>
    </bean>

      SessionManager中的SessionDao是自定义session会话存储的实现类 ,使用Redis来存储共享session,达到分布式部署目的。SessionDao的代码如下。

package com.jay.shiro;

import com.jay.redis.RedisService;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.apache.shiro.util.CollectionUtils;
import org.slf4j.Logger;

import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Set;

import static com.google.common.collect.Sets.newHashSet;
import static com.jay.util.DateTransformTools.DEFAULT_FORMAT;
import static com.jay.util.DateTransformTools.dateToDateStr;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * @author jay.zhou
 * @date 2019/1/15
 * @time 13:29
 */
public final class RedisSessionDao extends AbstractSessionDAO {

    private static final Logger LOGGER = getLogger(RedisSessionDao.class);
    /**
     * 此编码需要与 RedisServiceImpl 类中编码一致
     * 用于解析每个session的Key
     */
    private static final String DEFAULT_CHARSET = "UTF-8";

    /**
     * Redis接口服务
     */
    private RedisService redisService;

    /**
     * 过期时间
     */
    private Long expireSeconds;

    /**
     * shiro-redis的session对象前缀
     */
    private String keyPrefix = "shiro_redis_session:";

    public RedisService getRedisService() {
        return redisService;
    }

    public void setRedisService(RedisService redisService) {
        this.redisService = redisService;
    }

    public Long getExpireSeconds() {
        return expireSeconds;
    }

    public void setExpireSeconds(Long expireSeconds) {
        this.expireSeconds = expireSeconds;
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("更新Session:{}", session.getId());
        }
        this.saveSession(session);
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            LOGGER.error("session对象(或者sessionId)为空.");
            return;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("删除Session:{}", session.getId());
        }
        //通过sessionId删除session
        redisService.del(this.getByteKey(session.getId()));

    }

    /**
     * 统计当前活动的session
     *
     * @return 当前活动的session
     */
    @Override
    public Collection<Session> getActiveSessions() {
        final Set<Session> sessions = newHashSet();
        //获取缓存中匹配key值的所有键
        final Set<byte[]> keys = redisService.keys(this.keyPrefix + "*");
        if (!CollectionUtils.isEmpty(keys)) {
            for (byte[] key : keys) {
                //添加到set集合中
                byte[] bytes = redisService.get(key);
                Session session = SerializerUtil.deserialize(bytes);
                sessions.add(session);
            }
        }
        //shiro的session为我们提供了大量的API接口
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("==========>>统计活动Session(开始)总计活动Session:{}条.<<==========", sessions.size());
            for (Session session : sessions) {
                LOGGER.debug("ID:{}", session.getId());
                LOGGER.debug("有效期:{}秒", session.getTimeout() / 1000);
                LOGGER.debug("创建时间:{}", dateToDateStr(session.getStartTimestamp(), DEFAULT_FORMAT));
                LOGGER.debug("上次使用时间:{}", dateToDateStr(session.getStartTimestamp(), DEFAULT_FORMAT));
                LOGGER.debug(".......................................................................");
            }
            LOGGER.debug("==========>>统计活动Session(结束)总计活动Session:{}条.<<==========", sessions.size());
        }
        return sessions;
    }

    @Override
    protected Serializable doCreate(Session session) {
        //分配sessionId
        final Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        //保存session并存储到Redis集群中
        this.saveSession(session);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("创建Session:{}", sessionId);
        }
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            LOGGER.error("sessionId为空.");
            return null;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("读取Session:{}", sessionId);
        }
        //与saveSession是反操作,通过sessionId获取Key的字节数据
        final byte[] key = this.getByteKey(sessionId);
        //再通过key的字节数据找到value的字节数据
        final byte[] value = redisService.get(key);
        //最后再反序列化得到session对象
        return SerializerUtil.deserialize(value);
    }

    /**
     * 保存session
     * sessionId -> key[]
     * session   -> value[]
     *
     * @param session Session对象
     * @throws UnknownSessionException 未知Session异常
     */
    private void saveSession(Session session) throws UnknownSessionException {
        if (session == null || session.getId() == null) {
            LOGGER.error("session对象(或者sessionId)为空.");
            return;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("保存Session:{}", session.getId());
        }
        //sessionId -> key[]
        final byte[] key = getByteKey(session.getId());
        //session   -> value[]
        final byte[] value = SerializerUtil.serialize(session);
        session.setTimeout(getExpireSeconds());
        //save To Redis
        this.redisService.setEx(key, value, getExpireSeconds());
    }

    /**
     * 获得byte[]型的key
     *
     * @param sessionId sessionId
     * @return byte[]型的key
     */
    private byte[] getByteKey(Serializable sessionId) {
        final String preKey = this.keyPrefix + sessionId;
        return preKey.getBytes(Charset.forName(DEFAULT_CHARSET));
    }
}

三、测试

      我们需要将项目复制为两个,第一个项目的端口是8080,第二个项目的端口改为 9090,依次启动两个项目。测试的时候,确保你的Redis服务器是开着的。

      在8080端口项目中尝试访问受限制页面,会被重定向到登录页面。在登录页面输入jay / 123456,登录成功后。在9090端口项目一样点击第一个超链接尝试访问受限制页面,这次发现可以成功请求到后台JSON数据。然后在9090端口尝试退出登录,再回到8080端口的项目尝试访问受限制页面,发现用户已经退出,请求被重定向到登录页面。

     上述现象说明,使用Redis实现分布式Session共享成功。

      大宇能够成功搭建一个分布式项目,很大一部分原因就是站在巨人的肩膀上。特此鸣谢下方博客与博主。 

      参考文章: Apache shiro集群实现 (六)分布式集群系统下的高可用session解决方案---Session共享 

                         Shiro 分布式架构下 Session 的共享实现

                         shiro 学习之会话管理

                         Shiro在Spring的会话管理(session)

                         序列化工具

四、源码下载

     本章节项目源码:点击我下载源码

相关文章