需要知道的点
Shiro 的 Session 支持企业级的特性,例如分布式缓存。我们在 Spring Data Redis + Shiro 的方案中需要注意下以下几点:
- 无论 Redis 服务是单机还是集群模式,都需要注意 Session 对象的序列化与反序列化的问题;
- Shiro 的 Session:定义好的一个接口;Simple Session:一个它的简单实现,我们想要实现持久化就需要对它进行维护;
- EnterpriseCacheSessionDAO:Session 对象的增删改查,可以对 Session 对象进行下一步的定制化操作(个人理解),所以我们可以通过覆写它的方法来达到我们想要的持久化效果。以下 4 个方法是对 Session 的持久化处理:
- doCreate
- doUpdate
- doReadSession
- doDelete
- SessionManager:对 EnterpriseCacheSessionDAO 创建好的 Session 对象交给 SessionManager。它管理着 Session 的创建、操作以及清除等;DefaultSessionManager:具体实现,默认的 web 应用 Session 管理器,主要是涉及到 Session 和 Cookies,涉及到的行为:添加、删除 SessionId 到 Cookie、读取 Cookie 获得 SessionId;
- SessionId:得到 Session 的关键
- securityManager:这是 Shiro 框架的核心组件,可以把他看做是一个 Shiro 框架的全局管理组件,用于调度各种 Shiro 框架的服务。我们需要将自定义的 sessionManager 交给它
Session 持久化
上面写到如果想定制化我们的持久化效果,就必须覆写它的方法,所以我们需要新创建一个类 SessionRedisDao
来继承 EnterpriseCacheSessionDAO
类:
@Component
public class SessionRedisDao extends EnterpriseCacheSessionDAO {
/**
* 注入的是byteRedisTemplate,只用于byte[]类型的序列化存储在redis中
*/
private final RedisTemplate<String, byte[]> redisTemplate;
public SessionRedisDao(@Qualifier("byteRedisTemplate") RedisTemplate<String, byte[]> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 创建session,保存到数据库
* @param session
* @return
*/
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
System.out.println("===============【 " + sessionId + " 】 创建了session!================");
BoundValueOperations<String, byte[]> boundValueOperations = redisTemplate.boundValueOps(SHIRO_SESSION + sessionId.toString());
boundValueOperations.set(sessionToByte(session), 240, TimeUnit.MINUTES);
return sessionId;
}
/**
* 获取session
* @param sessionId
* @return
*/
@Override
protected Session doReadSession(Serializable sessionId) {
// 先从缓存中获取session,如果没有再去数据库中获取
Session session = super.doReadSession(sessionId);
//System.out.println("===============【 " + sessionId + " 】 获取了session!================");
if(session == null){
BoundValueOperations<String, byte[]> boundValueOperations = redisTemplate.boundValueOps(SHIRO_SESSION + sessionId.toString());
byte[] bytes = (boundValueOperations.get());
if(bytes != null && bytes.length > 0){
session = byteToSession(bytes);
}
}
return session;
}
/**
* 更新session的最后一次访问时间
* @param session
*/
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
System.out.println("===============【 " + session.getId() + " 】 更新了session!================");
BoundValueOperations<String, byte[]> boundValueOperations = redisTemplate.boundValueOps(SHIRO_SESSION + session.getId().toString());
boundValueOperations.set(sessionToByte(session), 240, TimeUnit.MINUTES);
}
/**
* 删除session
* @param session
*/
@Override
protected void doDelete(Session session) {
System.out.println("===============【 " + session.getId() + " 】 删除了session!================");
super.doDelete(session);
redisTemplate.delete(SHIRO_SESSION + session.getId().toString());
}
/**
* 把session对象转化为byte保存到redis中
* @param session
* @return
*/
public byte[] sessionToByte(Session session){
ByteArrayOutputStream bo = new ByteArrayOutputStream();
byte[] bytes = null;
try {
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(session);
bytes = bo.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return bytes;
}
/**
* 把byte还原为session
* @param bytes
* @return
*/
public Session byteToSession(byte[] bytes){
ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
ObjectInputStream in;
SimpleSession session = null;
try {
in = new ObjectInputStream(bi);
session = (SimpleSession) in.readObject();
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return session;
}
}
好像看起来和网上的其他技术文章的实现差不多,但是还有差啦(迷之台湾腔?)
首先是关于 RedisTemplate
客户端的注入使用:
private final RedisTemplate<String, byte[]> redisTemplate;
public SessionRedisDao(@Qualifier("byteRedisTemplate") RedisTemplate<String, byte[]> redisTemplate) {
this.redisTemplate = redisTemplate;
}
在这里看到 key 和 value 的类型 <String, byte[]>
,不是常规的 <String, Object>
,因为我在以下序列化工具中以 json 字符串的形式存储在 Redis:
FastJsonRedisSerializer
GenericJackson2JsonRedisSerializer
Jackson2JsonRedisSerializer
发现在执行 doUpdate
方法后,Redis 当中会增加一些 Simple Session 没有字段,比如 "valid":true
等等,所以在反序列化获取 Session 对象的过程中会抛出如下异常:
"Could not read JSON: Cannot construct instance of`org.apache.shiro.web.util.SavedRequest` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)↵ at [Source: (byte[])"["org.apache.shiro.session.mgt.SimpleSession"
我突然就想到了《码出高效》里面,有说过 POJO 类不要使用 isXxx 作为变量的形式
当然这里也没发现存在 isXxx
的成员变量,只看到了 isValid()
方法,以及 isStoped()
方法也没有对应的 stoped
成员属性。可能是在反序列化的过程中,通过 Redis 里的键值对发现,SimpleSession 并没有这个 boolean
类型的 valid
变量而导致错误。不知道是不是算 Shiro 的 Simple Session 一个 Bug。
解决手段
目前个人找到的解决的方法是使用 byte[]
字节流存储,且用默认的 JDK 序列化工具 JdkSerializationRedisSerializer
。
Redis 配置序列化工具
因为可能在代码其他处已经使用了其他序列化工具操作 Redis 了,所以这里建议重新写一个 Bean 的方法专门用于 Shiro 安全框架的 Session 操作:
@Bean(name = "byteRedisTemplate")
public RedisTemplate<String, byte[]> byteRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
// 全局开启AutoType,不建议使用
// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// 建议使用这种方式,小范围指定白名单
ParserConfig.getGlobalInstance().addAccept("com.zrtg.");
// 设置值(value)的序列化采用jdkSerializationRedisSerializer。
redisTemplate.setValueSerializer(jdkSerializationRedisSerializer);
redisTemplate.setHashValueSerializer(jdkSerializationRedisSerializer);
// 设置键(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
log.info("MatthewHan: [ byteRedisTemplate启动,鸡你实在是太美! ] ");
return redisTemplate;
}
然后在需要注入的地方,加入 @Qualifier
注解即可,像这样:@Qualifier("byteRedisTemplate") RedisTemplate<String, byte[]> redisTemplate
。
并入管理
@Bean
public DefaultWebSessionManager sessionManager(SessionRedisDao sessionRedisDao) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionIdCookie(remeberMeCookie());
sessionManager.setGlobalSessionTimeout(14400000L);
sessionManager.setDeleteInvalidSessions(true);
// 将写好的缓存sessionDao注入
sessionManager.setSessionDAO(sessionRedisDao);
sessionManager.setSessionValidationSchedulerEnabled(true);
return sessionManager;
}
/**
* 注入 securityManager
* 将写好的缓存sessionDao注入
* @param sessionRedisDao
* @param customRealmConfig
* @return
*/
@Bean
public SecurityManager securityManager(SessionRedisDao sessionRedisDao, CustomRealmConfig customRealmConfig) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(customRealmConfig);
// 注入Cookie记住我管理器
securityManager.setRememberMeManager(null);
securityManager.setSessionManager(sessionManager(sessionRedisDao));
return securityManager;
}
这里注意 rememberMe 的 Cookies 管理,以及 sessionManager.setGlobalSessionTimeout(14400000L);
和 Redis 设置的过期时间保持一致即可。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于