一、概述
Shiro 提供了类似于 Spring 的 Cache 抽象,即 Shiro 本身不实现 Cache,但是对 Cache 进行了又抽象,方便更换不同的底层 Cache 实现。
Shiro 提供的 Cache 接口:
public interface Cache<K, V> {
//根据Key获取缓存中的值
public V get(K key) throws CacheException;
//往缓存中放入key-value,返回缓存中之前的值
public V put(K key, V value) throws CacheException;
//移除缓存中key对应的值,返回该值
public V remove(K key) throws CacheException;
//清空整个缓存
public void clear() throws CacheException;
//返回缓存大小
public int size();
//获取缓存中所有的key
public Set<K> keys();
//获取缓存中所有的value
public Collection<V> values();
}
Shiro 提供的 CacheManager 接口:
public interface CacheManager {
//根据缓存名字获取一个Cache
public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}
Shiro 还提供了 CacheManagerAware 用于注入 CacheManager:
public interface CacheManagerAware {
//注入CacheManager
void setCacheManager(CacheManager cacheManager);
}
Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如 Realm)是否实现了 CacheManagerAware 并自动注入相应的 CacheManager。
在上一篇文章【使用Shiro实现用户授权功能】中,我们通过在doGetAuthorizationInfo(PrincipalCollection principalCollection)方法中,每次都去查询用户对应的角色和权限信息,实际上对于一个用户来说,其权限在短时间内基本是不会变化的。因此,Shiro提供了缓存功能,可以将权限缓存起来,避免频繁访问数据库获取权限信息,本篇文章主要介绍基于Redis和Ehcache缓存的实现,下面我们来看具体的实现。
二、Shiro集成Redis实现缓存
【a】引入redis-shiro依赖
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
<!-- 对象池,使用redis时必须引入 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
【b】application.yml中加入redis的配置
spring:
redis:
host: localhost
port: 6379
timeout: 0ms
lettuce:
pool:
连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1ms
连接池中的最大空闲连接 默认 8
max-idle: 8
连接池中的最小空闲连接 默认 0
min-idle: 0
【c】在Shiro全局配置类中配置Redis缓存管理器以及redis管理器
/**
* 配置Redis缓存管理器
*/
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
//设置redis管理器
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* 配置redis管理器
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
//设置一小时超时,单位是秒
redisManager.setExpire(3600);
return redisManager;
}
【d】将Redis缓存管理器加入给SecurityManager安全管理器管理
//设置缓存管理器
defaultWebSecurityManager.setCacheManager(redisCacheManager());
【e】新增一个需要访问权限的接口
@RequiresPermissions(value = "user:list")
@RequestMapping("/userList")
public String userList(){
return "userList";
}
新建userList.html:
<!doctype html>
<!--注意:引入thymeleaf的名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
userList page
</body>
</html>
success.html中加入如下超链接:
<div>跳转到userList.html: <a href="/userList">userList.html</a><br></div>
【f】启动项目
Caused by: java.lang.ClassNotFoundException: org.apache.shiro.event.EventBus
at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_121]
at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_121]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) ~[na:1.8.0_121]
at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_121]
... 25 common frames omitted
可见,项目启动报错:ClassNotFoundException: org.apache.shiro.event.EventBus。因为Maven依赖的jar包中缺少了EventBus这个class文件,原来maven工程中已经依赖了shiro-core1.4.0的版本 ,在shiro-redis依赖中使用了shiro-core-1.2版本,把1.4版本的排除了出去, 而这个类要在1.3版本上才有,所以需要排除1.2版本。
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
重新启动项目,这次启动没有报错了,但是当你访问list.html页面时,页面报如下的错误:
tried to access method redis.clients.jedis.JedisPool.returnResource
翻译一下,其实指的就是尝试从类org.crazycake.shiro.RedisManager访问方法,但是访问不到。
原因其实是redis.clients:jedis:3.1.0版本种的returnResource()方法访问修饰符变为protected,所以我们换低一点的版本即可:
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
<exclusion>
<artifactId>jedis</artifactId>
<groupId>redis.clients</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
再次重启项目,使用admin/123456进行登录:
依次访问userList.html和list.html两个需要授权的页面,如下图:
注意:需要首先启动好Redis服务,否则项目连不上Redis,会报错。
如下图,可以看到后台只打印出一次从数据库查询获取权限的日志信息,证明我们的redis缓存起效果。
以上就是关于Shiro整合Redis实现用户权限缓存功能,接下来我们再来看另外一种实现方式:Shiro整合Ehcache实现用户权限缓存功能。
三、Shiro集成Ehcache实现缓存
【a】引入Shiro跟Ehcachae整合相关依赖
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<!-- ehchache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
【b】新建Ehcache配置文件
在resource/config路径下新建shiro-ehcache-config.xml配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache"/>
<!--
defaultCache:默认的缓存配置信息,如果不加特殊说明,则所有对象按照此配置项处理
maxElementsInMemory:设置了缓存的上限,最多存储多少个记录对象
eternal:代表对象是否永不过期
timeToIdleSeconds:最大的发呆时间
timeToLiveSeconds:最大的存活时间
overflowToDisk:是否允许对象被写入到磁盘
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"/>
<!--
cache:为指定名称的对象进行缓存的特殊配置
name:指定对象的完整名
-->
<!-- 登录记录缓存锁定1小时 -->
<cache
name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true"/>
</ehcache>
【c】Shiro全局配置类中配置Ehcache
/**
* Ehcache缓存管理器
* @return
*/
@Bean
@Primary //指定ehcache为主要的缓存管理器
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:config/shiro-ehcache-config.xml");
return ehCacheManager;
}
【d】将缓存对象注入到SecurityManager中
//设置EhCacheManager缓存管理员
defaultWebSecurityManager.setCacheManager(ehCacheManager());
【e】启动项目测试
使用admin/123456进行登录:
依次访问userList.html和list.html两个需要授权的页面,如下图:
如下图,可以看到后台只打印出一次从数据库查询获取权限的日志信息,同样证明我们的ehcache缓存起效果。
注意,需要注释掉前面在shiro全局配置类中注入的redis缓存,否则启动会报错:因为发现两个缓存管理器。当然如果不注释的话,也可以使用@Primary注解指定某个缓存管理器为主要的管理器。
扩展知识:
Ehcache的配置详细说明如下:
1、以下属性是必须的:
1、 name: Cache的名称,必须是唯一的(ehcache会把这个cache放到HashMap里)。
2、 maxElementsInMemory:在内存中缓存的element的最大数目。
3、 maxElementsOnDisk:在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
4、 eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
5、 overflowToDisk: 如果内存中数据超过内存限制,是否要缓存到磁盘上。
2、以下属性是可选的:
1、 timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问。
2、 timeToLiveSeconds: 对象存活时间,指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问。
3、 diskPersistent: 是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。
4、 diskExpiryThreadIntervalSeconds: 对象检测线程运行时间间隔。标识对象状态的线程多长时间运行一次。
5、 diskSpoolBufferSizeMB: DiskStore使用的磁盘大小,默认值30MB。每个cache使用各自的DiskStore。
6、 memoryStoreEvictionPolicy: 如果内存中数据超过内存限制,向磁盘缓存时的策略。默认值LRU,可选FIFO、LFU。
3、缓存的3 种清空策略 :
1、 FIFO ,First In First Out (先进先出);
2、 LFU , Less Frequently Used (最少使用)。意思是一直以来最少被使用的。缓存的元素有一个hit 属性,hit 值最小的将会被清出缓存;
3、 LRU ,Least Recently Used(最近最少使用)。 (ehcache 默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存;
四、总结
本篇文章主要总结了Shiro提供的缓存功能,能大大减少授权的时候对数据库的查询,通过配置两种缓存管理器:Redis和Ehcache缓存管理器。在实际项目中,可以根据需求配置两种缓存管理器,实现用户高效授权操作。