Shiro 解决分布式 Session

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 在分布式系统中,会话管理是一个重要的问题。Shiro框架提供了一种解决方案,通过其会话管理组件来处理分布式会话。本文演示通过RedisSessionManager解决分布式会话问题。

@[TOC]

前言

在分布式系统中,会话管理是一个重要的问题。Shiro框架提供了一种解决方案,通过其会话管理组件来处理分布式会话。本文演示通过RedisSessionManager解决分布式会话问题。

Shiro 会话管理组件

Shiro框架的会话管理组件提供了会话的创建、维护、删除和失效等操作。在分布式环境中,多个应用服务器可能需要共享会话状态。为了实现这一点,Shiro框架提供了一些会话管理器实现,其中包括:

  • DefaultSessionManager:默认会话管理器,提供了基本的会话管理功能。在分布式环境中,如果需要在多个应用服务器之间共享会话状态,则需要使用其他会话管理器。
  • EnterpriseCacheSessionDAO:基于缓存的会话管理器,使用缓存来存储会话状态。在分布式环境中,可以使用分布式缓存来实现会话状态的共享。
  • RedisSessionManager:基于Redis的会话管理器,使用Redis来存储会话状态。Redis是一个分布式缓存系统,可以在多个应用服务器之间共享会话状态。

使用这些会话管理器实现,可以将会话状态存储在分布式缓存中,以便在多个应用服务器之间共享。

配置 RedisSessionManager

导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

编写配置

    spring:
      redis:
        host: 192.168.0.10
        port: 6379
        password: xxxxx

声明SessionDAO的实现类,并重写核心方法

  @Component
  public class RedisSessionDAO extends AbstractSessionDAO {
   

      @Resource
      private RedisTemplate redisTemplate;

      // 存储到Redis时,sessionId作为key,Session作为Value
      // sessionId就是一个字符串
      // Session可以和sessionId绑定到一起,绑定之后,可以基于Session拿到sessionId
      // 需要给Key设置一个统一的前缀,这样才可以方便通过keys命令查看到所有关联的信息

      private final String SHIOR_SESSION = "session:";

      @Override
      protected Serializable doCreate(Session session) {
   
          System.out.println("Redis---doCreate");
          //1. 基于Session生成一个sessionId(唯一标识)
          Serializable sessionId = generateSessionId(session);

          //2. 将Session和sessionId绑定到一起(可以基于Session拿到sessionId)
          assignSessionId(session, sessionId);

          //3. 将 前缀:sessionId 作为key,session作为value存储
          redisTemplate.opsForValue().set(SHIOR_SESSION + sessionId,session,30, TimeUnit.MINUTES);

          //4. 返回sessionId
          return sessionId;
      }

       @Override
      protected Session doReadSession(Serializable sessionId) {
   
          //1. 基于sessionId获取Session (与Redis交互)
          if (sessionId == null) {
   
              return null;
          }
          Session session = (Session) redisTemplate.opsForValue().get(SHIOR_SESSION + sessionId);
          if (session != null) {
   
              redisTemplate.expire(SHIOR_SESSION + sessionId,30,TimeUnit.MINUTES);
          }
          return session;
      }

      @Override
      public void update(Session session) throws UnknownSessionException {
   
          System.out.println("Redis---update");
          //1. 修改Redis中session
          if(session == null){
   
              return ;
          }
          redisTemplate.opsForValue().set(SHIOR_SESSION + session.getId(),session,30, TimeUnit.MINUTES);
      }

      @Override
      public void delete(Session session) {
   
          // 删除Redis中的Session
          if(session == null){
   
              return ;
          }
          redisTemplate.delete(SHIOR_SESSION + session.getId());
      }

      @Override
      public Collection<Session> getActiveSessions() {
   
          Set keys = redisTemplate.keys(SHIOR_SESSION + "*");

          Set<Session> sessionSet = new HashSet<>();
          // 尝试修改为管道操作,pipeline(Redis的知识)
          for (Object key : keys) {
   
              Session session = (Session) redisTemplate.opsForValue().get(key);
              sessionSet.add(session);
          }
          return sessionSet;
      }
  }

将RedisSessionDAO交给SessionManager

  @Bean
  public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
   
      DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
      sessionManager.setSessionDAO(sessionDAO);
      return sessionManager;
  }

将SessionManager注入到SecurityManager

  @Bean
  public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
   
      DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
      securityManager.setRealm(realm);
      securityManager.setSessionManager(sessionManager);
      return securityManager;
  }

使用 RedisSession 问题

将传统的基于Web容器或者ConcurrentHashMap切换为Redis之后,发现每次请求需要访问多次Redis服务,这个访问的频次会出现很长时间的IO等待,对每次请求的性能减低了,并且对Redis的压力也提高了。

基于装饰者模式重新声明SessionManager中提供的retrieveSession方法,让每次请求先去request域中查询session信息,request域中没有,再去Redis中查询

  public class DefaultRedisWebSessionManager extends DefaultWebSessionManager {
   

      @Override
      protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
   
          // 通过sessionKey获取sessionId
          Serializable sessionId = getSessionId(sessionKey);

          // 将sessionKey转为WebSessionKey
          if(sessionKey instanceof WebSessionKey){
   
              WebSessionKey webSessionKey = (WebSessionKey) sessionKey;
              // 获取到request域
              ServletRequest request = webSessionKey.getServletRequest();
              // 通过request尝试获取session信息
              Session session = (Session) request.getAttribute(sessionId + "");
              if(session != null){
   
                  System.out.println("从request域中获取session信息");
                  return session;
              }else{
   
                  session = retrieveSessionFromDataSource(sessionId);
                  if (session == null) {
   
                      //session ID was provided, meaning one is expected to be found, but we couldn't find one:
                      String msg = "Could not find session with ID [" + sessionId + "]";
                      throw new UnknownSessionException(msg);
                  }
                  System.out.println("Redis---doReadSession");
                  request.setAttribute(sessionId + "",session);
                  return session;
              }
          }
          return null;
      }
  }

配置DefaultRedisWebSessionManager到SecurityManager中

  @Bean
  public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
   
      DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
      sessionManager.setSessionDAO(sessionDAO);
      return sessionManager;
  }

Shiro的授权缓存

如果后台接口存在授权操作,那么每次请求都需要去数据库查询对应的角色信息和权限信息,对数据库来说,这样的查询压力太大了。

在Shiro中,发现每次在执行自定义Realm的授权方法查询数据库之前,会有一个执行Cache的操作。先从Cache中基于一个固定的key去查询角色以及权限的信息。

只需要提供好响应的CacheManager实例,还要实现一个与Redis交互的Cache对象,将Cache对象设置到CacheManager实例中。

将上述设置好的CacheManager设置到SecurityManager对象中

实现RedisCache

@Component
public class RedisCache<K, V> implements Cache<K, V> {
   

    @Autowired
    private RedisTemplate redisTemplate;

    private final String CACHE_PREFIX = "cache:";

    /**
     * 获取授权缓存信息
     * @param k
     * @return
     * @throws CacheException
     */
    @Override
    public V get(K k) throws CacheException {
   
        V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
        if(v != null){
   
            redisTemplate.expire(CACHE_PREFIX + k,15, TimeUnit.MINUTES);
        }
        return v;
    }

    /**
     * 存放缓存信息
     * @param k
     * @param v
     * @return
     * @throws CacheException
     */
    @Override
    public V put(K k, V v) throws CacheException {
   
        redisTemplate.opsForValue().set(CACHE_PREFIX + k,v,15,TimeUnit.MINUTES);
        return v;
    }

    /**
     * 清空当前缓存
     * @param k
     * @return
     * @throws CacheException
     */
    @Override
    public V remove(K k) throws CacheException {
   
        V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
        if(v != null){
   
            redisTemplate.delete(CACHE_PREFIX + k);
        }
        return v;
    }

    /**
     * 清空全部的授权缓存
     * @throws CacheException
     */
    @Override
    public void clear() throws CacheException {
   
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        redisTemplate.delete(keys);
    }

    /**
     * 查看有多个权限缓存信息
     * @return
     */
    @Override
    public int size() {
   
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        return keys.size();
    }

    /**
     * 获取全部缓存信息的key
     * @return
     */
    @Override
    public Set<K> keys() {
   
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        return keys;
    }

    /**
     * 获取全部缓存信息的value
     * @return
     */
    @Override
    public Collection<V> values() {
   
        Set values = new HashSet();
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        for (Object key : keys) {
   
            Object value = redisTemplate.opsForValue().get(key);
            values.add(value);
        }
        return values;
    }
}

实现CacheManager

@Component
public class RedisCacheManager implements CacheManager {
   
    @Autowired
    private RedisCache redisCache;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
   
        return redisCache;
    }
}

将RedisCacheManager配置到SecurityManager

@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager){
   
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(realm);
    securityManager.setSessionManager(sessionManager);
    // 设置CacheManager,提供与Redis交互的Cache对象
    securityManager.setCacheManager(redisCacheManager);
    return securityManager;
}

启用授权缓存后,Shiro框架会将授权数据缓存在内存中,以便快速进行授权验证。在需要进行授权验证时,Shiro框架会首先从缓存中查找授权数据,如果缓存中不存在,则会从数据源中获取授权数据,并将其缓存到内存中。

需要注意的是,授权缓存可能会导致数据的过时。因此,在启用授权缓存时,需要根据具体的业务需求,设置合适的缓存失效时间和更新机制,以确保授权数据的实时性和准确性。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
存储 负载均衡 NoSQL
分布式Session
分布式Session
58 0
|
7月前
|
存储 缓存 负载均衡
分布式系统Session一致性问题
分布式系统Session一致性问题
84 0
|
7月前
|
负载均衡 算法 NoSQL
聊聊分布式应用中负载均衡技术和Session一致性
聊聊分布式应用中负载均衡技术和Session一致性
81 0
|
负载均衡 算法 NoSQL
分布式系列教程(15) - 解决分布式Session一致性问题
分布式系列教程(15) - 解决分布式Session一致性问题
151 0
|
存储 NoSQL Java
Spring Session分布式会话管理
Spring Session分布式会话管理
98 0
|
6月前
|
NoSQL Java 应用服务中间件
大厂面试必备:如何轻松实现分布式Session管理?
这篇文章介绍三种分布式Session的实现方案:基于JWT的Token、基于Tomcat的Redis和基于Spring的Redis。JWT方案通过生成Token存储用户信息,实现无状态、可扩展的会话管理,但可能增加请求负载且数据安全性较低。Tomcat与Redis结合,通过配置Tomcat和Redis,实现Session集中管理和高性能存储,但配置相对复杂。Spring整合Redis适用于SpringBoot和SpringCloud项目,集成方便,扩展性强,但同样依赖外部Redis服务。每种方法有其优缺点,适用场景不同。作者小米是一个技术爱好者,欢迎关注其微信公众号“软件求生”获取更多技术内容
254 4
|
2月前
|
存储 缓存 NoSQL
分布式架构下 Session 共享的方案
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求、系统架构和性能要求等因素,选择合适的 Session 共享方案。同时,还需要不断地进行优化和调整,以确保系统的稳定性和可靠性。
|
3月前
|
存储 NoSQL Java
使用springSession完成分布式session
本文介绍了如何使用 `spring-session` 实现分布式 Session 管理,并提供了将 Session 存储在 Redis 中的具体配置示例。通过配置相关依赖及 Spring 的配置文件,可以轻松实现 Session 的分布式存储。示例中详细展示了所需的 Maven 依赖、Spring 配置及过滤器配置,并给出了启动项目后在 Redis 中查看 Session 数据的方法。
|
4月前
|
存储 NoSQL Java
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
这篇文章是关于Java面试中的分布式架构问题的笔记,包括分布式架构下的Session共享方案、RPC和RMI的理解、分布式ID生成方案、分布式锁解决方案以及分布式事务解决方案。
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
|
6月前
|
存储 缓存 算法
分布式Session共享解决方案
分布式Session共享解决方案
63 0