所有服务器的session信息都存储到了同一个Redis集群中,即所有的服务都将 Session 的信息存储到 Redis 集群中,无论是对 Session 的注销、更新都会同步到集群中,达到了 Session 共享的目的。
Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。
在实际工作中我们建议使用外部的缓存设备(包括Redis)来共享 Session,避免单个服务器节点挂掉而影响服务,共享数据都会放到外部缓存容器中。
重要提示:
Shiro的Sessin实现是SimpleSession。SimpleSession的所有的属性是transient,所以一般情况下,将其存放到Redis中会丢失所有的属性值。为了解决这种问题,必须要使用JDK序列化策略来序列化SimpleSession,因此这就是为什么我们把SimpleSession转成字节存储的原因。所以说,RedisTemplate的value的序列化策略必须要使用JdkSerializationRedisSerializer。
为了能够让多个服务器共享Session,我们需要把Session存储到外部的缓存设备。
我们需要让session在集群中共享,就需要替换Shiro默认的sessionManager。我们需要使用DefaultWebSessionManager作为SessionManager。
...
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 getActiveSessions() {
final Set sessions = newHashSet();
//获取缓存中匹配key值的所有键
final Set 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)
序列化工具
本章节项目源码:点击我下载源码
----------------------------------------------------分割线-------------------------------------------------------
下一篇: 第十四节 Shiro缓存机制
阅读更多:跟着大宇学Shiro目录贴