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.
- pom. Introducing dependencies into XML
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- In application Configuring Redis in YML
Spring: ... # Redis configuration redis: port: 6379 host: localhost database: 0
- Create cache package in shiro package
- 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.
- 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.
- 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; } }
- 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()); }