[Shiro]5. Integrating Redis to realize caching

Posted by everisk on Wed, 19 Jan 2022 05:29:28 +0100

In the actual development of front-end and back-end, we will use annotations to control permissions. When performing authentication or authorization operations, Shiro will query the identity or permission information in the DB. It is known that identity information and authority information will not change frequently and are very complex. If many users operate the system at the same time, Shiro needs to query the identity or permission in the DB for each operation, which undoubtedly increases the pressure on the database and consumes a lot of computing resources.

In order to avoid the above problems, we will add cache when designing identity and permission.

The so-called caching means that if the system has authenticated or authorized the user once, it caches the user's identity information or authority information. When the user is authenticated or authorized again, Shiro directly obtains the identity information and authority information to the user from the cache.

1. Implementation process

Shiro provides CacheManager as the cache manager. The specific implementation process is as follows

2. Concrete realization

Shiro's default cache is EhCache, which can only implement local cache. If the application server goes down, the cache data will be lost. In actual production practice, distributed caching is generally implemented in cooperation with Redis. The cached data is independent of the application server to improve the security of the data.

  1. pom. Introducing dependencies into XML
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. In application Configuring Redis in YML
    Spring:
      ...
      # Redis configuration
      redis:
        port: 6379
        host: localhost
        database: 0
    
  3. Create cache package in shiro package
  4. Create Redis cache manager in cache package
    public class RedisCacheManager implements CacheManager {
    
        // This method is called every time the cache is executed, and s is automatically injected 
        // Parameter s is the name of the authentication cache or authorization cache set in ShiroConfig
        @Override
        public <K, V> Cache<K, V> getCache(String s) throws CacheException {
            // Automatically go to RedisCahce to find the specific implementation
            return new RedisCache<K, V>(s);
        }
        
    }
    

    Shiro provides a global cache manager interface CacheManager. If you want to implement a custom cache manager, you must let the custom cache manager implement the CacheManager interface.

  5. Create a IDS cache in the cache package
    public class RedisCache<K, V> implements Cache<K, V> {
    
        // Authentication cache or authorization cache name
        private String cacheName;
    
        public RedisCache() {
    
        }
    
        public RedisCache(String cacheName) {
            this.cacheName = cacheName;
        }
    
        // Get RedisTemplate instance
        private RedisTemplate getRedisTemplate() {
            // Remove the RedisTemplate instance from the factory
            RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
            // Set the serialization rule of Key to string
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            // Set the serialization rule of field in Hash to string
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    
            return redisTemplate;
        }
    
        // Get cache
        @Override
        public V get(K k) throws CacheException {
            return (V) getRedisTemplate().opsForHash().get(this.cacheName, k.toString())
        }
    
        // Store in cache
        @Override
        public V put(K k, V v) throws CacheException {
            getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
    
            return null;
        }
    
        // Delete cache
        @Override
        public V remove(K k) throws CacheException {
            return (V) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());;
        }
    
        // Empty all caches
        @Override
        public void clear() throws CacheException {
            getRedisTemplate().delete(this.cacheName);
        }
    
        // Number of caches
        @Override
        public int size() {
            return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
        }
    
        // Get all keys
        @Override
        public Set<K> keys() {
            return getRedisTemplate().opsForHash().keys(this.cacheName);
        }
    
        // Get all values
        @Override
        public Collection<V> values() {
            return getRedisTemplate().opsForHash().values(this.cacheName);
        }
    
    }
    

    Cache < K, V > is the real cache in the bottom layer of CacheManager. Therefore, a RedisCache needs to be created to truly implement custom cache. RedisCache also needs to implement cache interface.

    All interfaces in RedisCache are implemented by Redis, so as to realize the integration of Shiro and Redis. As for when to call what interface in RedisCache, Shiro decides, we just need to define.

    Redis uses Hash for Shiro identity and permission management. Key corresponds to cacheName, field corresponds to k, and value corresponds to v.

  6. Configure cache manager in ShiroConfig
    @Configuration
    public class ShiroConfig {
    
        // Create ShiroFilter
        @Bean
        public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
            ...
        }
    
        // Create a SecurityManager with Web attributes
        @Bean
        public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") Realm realm) {
            ...
        }
    
        // Create custom Realm
        @Bean
        public Realm getRealm() {
           	...
    
            // Injection cache manager
            userRealm.setCacheManager(new RedisCacheManager());
            // Turn on global cache
            userRealm.setCachingEnabled(true);
            // Open the authentication cache and name it (the real authentication cache name is cacheName)
            userRealm.setAuthenticationCachingEnabled(true);
            userRealm.setAuthenticationCacheName("authenticationCache");
            // Open the authorization cache and name it (the real authorization cache name is full package name + cacheName)
            userRealm.setAuthorizationCachingEnabled(true);
            userRealm.setAuthorizationCacheName("authorizationCache");
    
            return userRealm;
        }
    
    }
    
  7. Serializing and deserializing Salt

    According to the above configuration method, Salt is directly stored by ByteSource and is not serialized.

    // Get the encrypted password and Salt, Shiro automatically authenticates
    if (user != null) {
        return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
    }
    

    During Shiro authentication, Salt is also stored in the cache along with username and Password. Username and Password are serialized and deserialized by String, while Salt (ByteSource) also needs to be serialized and deserialized.

Create salt package in shiro package, and create ByteSource that can be serialized and deserialized by Redis in salt package

public class MyByteSource implements ByteSource, Serializable {

    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MyByteSource() {

 }

 public MyByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MyByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MyByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MyByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MyByteSource(File file) {
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
    }

    public MyByteSource(InputStream stream) {
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    @Override
    public byte[] getBytes() {
        return this.bytes;
    }

    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    @Override
    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    @Override
    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    @Override
    public String toString() {
        return this.toBase64();
    }

    @Override
    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource)o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }

}

Note that MyByteSource cannot be inherited from SimpleByteSource because SimpleByteSource has no parameterless structure, so it can only realize serialization but not deserialization. When Salt is deserialized by Redis, it needs to call the parameterless structure of MyByteSource, so MyByteSource can only realize ByteSource.

// Get the encrypted password and Salt, Shiro automatically authenticates
if (user != null) {
    return new SimpleAuthenticationInfo(username, user.getPassword(), new MyByteSource(user.getSalt()), this.getName());
}


 

Topics: Redis Shiro