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

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

需要知道的点

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 回帖 • 76 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖

相关帖子

欢迎来到这里!

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

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