前言
在 手擼 Java Web RBAC 權限管理 中,我們自己實現了一個簡易的 RBAC 權限管理框架,且我們也提到了一些缺陷,其中一點就是 : 每次請求需要授權的頁面都會去數據庫查詢此用戶對應的權限數據和角色數據,太耗費資源,應該進行緩存。
本章我們就來講講如何將 Shiro 中的授權數據緩存到 Redis 中。
API
Shiro 爲授權數據的緩存提供了兩個藉口,一個是 CacheManager
,一個是 Cache
。
根據這兩個接口,我們完全可以將授權數據緩存到任何地方,包括 redis
、ehcache
、內存等。
Redis
既然我們要緩存到 Redis 中,我們需要搭建 Redis 環境,並導入 Redis 工具類:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
新建配置文件 spring-redis.xml
:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="jedisPool" class="redis.clients.jedis.JedisPool"> <!-- Jedis 配置信息 --> <constructor-arg name="poolConfig" ref="jedisPoolConfig"/> <!-- Redis URL --> <constructor-arg name="host" value="127.0.0.1"/> <!-- Redis 端口--> <constructor-arg name="port" value="6379"/> <!-- Redis 密碼 --> <!--<constructor-arg value=""/>--> </bean> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <!-- 最大連接數 --> <property name="maxTotal" value="500"/> <!-- 最大閒置 --> <property name="maxIdle" value="100"/> <!-- 最小閒置 --> <property name="minIdle" value="10"/> <!-- 最大等待 --> <property name="maxWaitMillis" value="5000"/> <!-- 可以獲取 --> <property name="testOnBorrow" value="true"/> </bean> </beans>
Cache
我們來創建一個 RedisCache
繼承自 org.apache.shiro.cache.Cache
,來實現它的方法:
package im.zhaojun.cache; import im.zhaojun.util.JedisUtil; import org.apache.log4j.Logger; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.springframework.stereotype.Component; import org.springframework.util.SerializationUtils; import javax.annotation.Resource; import java.util.Collection; import java.util.Set; @Component public class RedisCache<K, V> implements Cache<K, V> { private static final Logger logger = Logger.getLogger(RedisCache.class); @Resource private JedisUtil jedisUtil; private final String CACHE_PREFIX = "shiro-cache:"; private byte[] getKeyBytes(K k) { return (CACHE_PREFIX + k).getBytes(); } @Override public V get(K k) throws CacheException { logger.info("從 Redis 中讀取授權信息..."); byte[] key = getKeyBytes(k); byte[] value = jedisUtil.get(key); if (value != null) { return (V) SerializationUtils.deserialize(value); } return null; } @Override public V put(K k, V v) throws CacheException { byte[] key = getKeyBytes(k); byte[] value = SerializationUtils.serialize(v); jedisUtil.set(key, value); jedisUtil.expire(key, 600); return v; } @Override public V remove(K k) throws CacheException { byte[] key = getKeyBytes(k); byte[] value = jedisUtil.get(key); jedisUtil.del(key); if (value != null) { SerializationUtils.deserialize(value); } return null; } @Override public void clear() throws CacheException { jedisUtil.delKeysByPrefix(CACHE_PREFIX); } @Override public int size() { return jedisUtil.getKeysByPrefix(CACHE_PREFIX).size(); } @Override public Set<K> keys() { return (Set<K>) jedisUtil.getKeysByPrefix(CACHE_PREFIX); } @Override public Collection<V> values() { return jedisUtil.getValuesByPrefix(CACHE_PREFIX); } }
其中沒什麼難點,只是對 redis 的基本增刪改查操作,由於是存儲到 redis 中,所以我們爲緩存數據的 key 添加了前綴,以便再次獲取。
CacheManager
我們創建一個 RedisCacheManager
類來繼承自 org.apache.shiro.cache.AbstractCacheManager
,當然你也可以直接繼承自 org.apache.shiro.cacheCacheManager
:
package im.zhaojun.cache; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.AbstractCacheManager; import javax.annotation.Resource; @Component public class RedisCacheManager extends AbstractCacheManager { @Resource private RedisCache redisCache; @Override protected Cache createCache(String s) throws CacheException { return redisCache; } }
這裏在 createCache()
方法中返回我們的自定義 RedisCache
對象即可。
Spring
然後我們將 RedisCacheManager
配置到 securityManager
中:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm"/> <property name="cacheManager" ref="redisCacheManager"/> </bean>
以及將 spring-redis.xml
配置到 web.xml
中:
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:spring.xml, classpath:spring-shiro.xml, classpath:spring-redis.xml </param-value> </context-param>
Test
然後我們分別在 Realm
的 doGetAuthorizationInfo
方法和 RedisCache
的 get
方法中分別打印一條日誌,看何時會訪問數據庫,何時會訪問 Redis
緩存的數據。
- 首先是未認證的情況下,訪問需要權限的的頁面,不會輸出任何信息,因爲需要認證後,纔會根據認證信息去獲取授權現象,沒有認證時,會直接攔截。
- 認證之後,訪問需要授權的頁面,會輸入如下信息: im.zhaojun.cache.RedisCache 15:09:14,015 INFO RedisCache:30 - 從 Redis 中讀取授權信息... im.zhaojun.realm.MyRealm 15:09:14,016 INFO MyRealm:23 - 從數據庫中讀取授權信息... 由此可見,Shiro 會先去 Redis 中取數據,如果 Redis 中沒有,再去 Realm(數據庫) 中取。 然後再次訪問這個頁面,輸入: im.zhaojun.cache.RedisCache 15:11:13,351 INFO RedisCache:30 - 從 Redis 中讀取授權信息... 因爲緩存中已經有了,就不再去數據庫中查詢了。
小結
其實頻繁從 Redis 中讀取也是比較浪費資源的, Redis 的連接同樣寶貴,最好的辦法還是直接存儲在內存中,但也是各有利弊,需要根據實際項目來決定使用哪種方案。
放到 Redis 的好處是:可以用來做跨項目/機器的數據緩存,可以集羣,持久化等。 放到內存的好處是:速度快,使用方便快捷。 但使用這種緩存還有一個比較重要的事情,就是當數據庫中的授權數據發生修改時,也要記得刷新緩存中的數據,不然會出現數據錯亂,實現方式可以通過直接覆蓋緩存,消息隊列通知等方式,需要根據不同項目來選區不同方式,由於篇幅原因這裏不再展開講了。
本章代碼地址 : https://github.com/zhaojun1998/Premission-Study/tree/master/Permission-Shiro-08/