Shiro solves distributed session

Preface

In distributed systems, session management is an important issue. The Shiro framework provides a solution for handling distributed sessions through its session management component. This article demonstrates how to solve distributed session problems through RedisSessionManager.

Shiro session management component

The session management component of the Shiro framework provides operations such as session creation, maintenance, deletion, and invalidation. In a distributed environment, multiple application servers may need to share session state. To enable this, the Shiro framework provides several session manager implementations, including:

  • DefaultSessionManager: The default session manager provides basic session management functions. In a distributed environment, if session state needs to be shared among multiple application servers, additional session managers are required.
  • EnterpriseCacheSessionDAO: A cache-based session manager that uses cache to store session state. In a distributed environment, distributed cache can be used to share session state.
  • RedisSessionManager: A Redis-based session manager that uses Redis to store session state. Redis is a distributed caching system that can share session state among multiple application servers.

Using these session manager implementations, session state can be stored in a distributed cache for sharing among multiple application servers.

Configure RedisSessionManager

Import dependencies

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

Write configuration

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

Declare the implementation class of SessionDAO and override the core methods

  @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;
      }
  }

Hand over RedisSessionDAO to SessionManager

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

Inject SessionManager into SecurityManager

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

Problems using RedisSession

After switching from the traditional web container or ConcurrentHashMap to Redis, it was found that each request needs to access the Redis service multiple times. This frequency of access will cause a long IO wait, which reduces the performance of each request and affects Redis. The pressure has also increased.

Re-declare the retrieveSession method provided in SessionManager based on the decorator pattern, so that each request first queries the session information in the request field. If there is no session information in the request field, then queries it in 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;
      }
  }

Configure DefaultRedisWebSessionManager to SecurityManager

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

Shiro's authorization cache

If there is authorization operation in the background interface, then each request needs to go to the database to query the corresponding role information and permission information. For the database, such query pressure is too great.

In Shiro, it was found that every time before executing the custom Realm authorization method to query the database, there will be a Cache operation. First, query the role and permission information from the Cache based on a fixed key.

You only need to provide a responsive CacheManager instance, implement a Cache object that interacts with Redis, and set the Cache object to the CacheManager instance.

Set the CacheManager set above to the SecurityManager object

Implement 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;
    }
}

Implement 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;
    }
}

Configure RedisCacheManager to 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;
}

When authorization caching is enabled, the Shiro framework caches authorization data in memory for fast authorization verification. When authorization verification is required, the Shiro framework will first look up the authorization data from the cache. If it does not exist in the cache, it will obtain the authorization data from the data source and cache it in memory.

It should be noted that authorization caching may cause data to become outdated. Therefore, when enabling authorization cache, it is necessary to set appropriate cache expiration time and update mechanism according to specific business needs to ensure the real-time and accuracy of authorization data.

Guess you like

Origin blog.csdn.net/qq_28314431/article/details/133046445