前言

在分布式系统中,会话管理是一个重要的问题。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框架会首先从缓存中查找授权数据,如果缓存中不存在,则会从数据源中获取授权数据,并将其缓存到内存中。

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

09-20 06:54