Shiro 与分布式 Session 与 Redis 的那些坑

本贴最后更新于 1809 天前,其中的信息可能已经事过景迁

需要知道的点

Shiro 的 Session 支持企业级的特性,例如分布式缓存。我们在 Spring Data Redis + Shiro 的方案中需要注意下以下几点:

  1. 无论 Redis 服务是单机还是集群模式,都需要注意 Session 对象的序列化与反序列化的问题;
  2. Shiro 的 Session:定义好的一个接口;Simple Session:一个它的简单实现,我们想要实现持久化就需要对它进行维护;
  3. EnterpriseCacheSessionDAO:Session 对象的增删改查,可以对 Session 对象进行下一步的定制化操作(个人理解),所以我们可以通过覆写它的方法来达到我们想要的持久化效果。以下 4 个方法是对 Session 的持久化处理:
    • doCreate
    • doUpdate
    • doReadSession
    • doDelete
  4. SessionManager:对 EnterpriseCacheSessionDAO 创建好的 Session 对象交给 SessionManager。它管理着 Session 的创建、操作以及清除等;DefaultSessionManager:具体实现,默认的 web 应用 Session 管理器,主要是涉及到 Session 和 Cookies,涉及到的行为:添加、删除 SessionId 到 Cookie、读取 Cookie 获得 SessionId;
  5. SessionId:得到 Session 的关键
  6. 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:

  1. FastJsonRedisSerializer
  2. GenericJackson2JsonRedisSerializer
  3. 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。

isValid.png

解决手段

目前个人找到的解决的方法是使用 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 设置的过期时间保持一致即可。

  • Shiro
    20 引用 • 29 回帖
  • 分布式
    80 引用 • 149 回帖 • 4 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 21 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...