需要知道的点 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
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 @Component public class SessionRedisDao extends EnterpriseCacheSessionDAO { private final RedisTemplate<String, byte []> redisTemplate; public SessionRedisDao (@Qualifier("byteRedisTemplate") RedisTemplate<String, byte []> redisTemplate) { this .redisTemplate = redisTemplate; } @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; } @Override protected Session doReadSession (Serializable sessionId) { Session session = super .doReadSession(sessionId); 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; } @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); } @Override protected void doDelete (Session session) { System.out.println("===============【 " + session.getId() + " 】 删除了session!================" ); super .doDelete(session); redisTemplate.delete(SHIRO_SESSION + session.getId().toString()); } 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; } 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
客户端的注入使用:
1 2 3 4 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对象的过程中会抛出如下异常:
1 "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操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Bean(name = "byteRedisTemplate") public RedisTemplate<String, byte []> byteRedisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, byte []> redisTemplate = new RedisTemplate <>(); redisTemplate.setConnectionFactory(redisConnectionFactory); JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer (); ParserConfig.getGlobalInstance().addAccept("com.zrtg." ); redisTemplate.setValueSerializer(jdkSerializationRedisSerializer); redisTemplate.setHashValueSerializer(jdkSerializationRedisSerializer); redisTemplate.setKeySerializer(new StringRedisSerializer ()); redisTemplate.setHashKeySerializer(new StringRedisSerializer ()); redisTemplate.afterPropertiesSet(); log.info("MatthewHan: [ byteRedisTemplate启动,鸡你实在是太美! ] " ); return redisTemplate; }
然后在需要注入的地方,加入@Qualifier
注解即可,像这样:@Qualifier("byteRedisTemplate") RedisTemplate<String, byte[]> redisTemplate
。
并入管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Bean public DefaultWebSessionManager sessionManager (SessionRedisDao sessionRedisDao) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager (); sessionManager.setSessionIdCookie(remeberMeCookie()); sessionManager.setGlobalSessionTimeout(14400000L ); sessionManager.setDeleteInvalidSessions(true ); sessionManager.setSessionDAO(sessionRedisDao); sessionManager.setSessionValidationSchedulerEnabled(true ); return sessionManager; } @Bean public SecurityManager securityManager (SessionRedisDao sessionRedisDao, CustomRealmConfig customRealmConfig) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(customRealmConfig); securityManager.setRememberMeManager(null ); securityManager.setSessionManager(sessionManager(sessionRedisDao)); return securityManager; }
这里注意rememberMe的Cookies管理,以及sessionManager.setGlobalSessionTimeout(14400000L);
和Redis设置的过期时间保持一致即可。