缓存
- Shiro提供了类似于Spring的Cache抽象,即它本身不实现cache,但是对cache进行了抽象,方便更换底层cache实现(如,Ehcache,Hazelcast,OSCache,Terracotta,Coherence,GigaSpaces,JBossCache)。
- 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();
}
- CacheManager接口:从接口方法来看就是用来获取cache的。
public interface CacheManager {
//根据缓存名字获取一个Cache实例,不存在则新建一个
public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}
从上面来看,我们定义缓存要做两件事,一是实现Cache接口(shiro-ehcache.xml),二是实现CacheManager接口(shiro.ini中注册EhCacheManager的bean)。
Shiro数据的缓存方式分为两类,一是将数据存储到本地,一是存储到集中式存储中间件,如,redis或Memcached。若使用后者,当页面使用了大量shiro标签时(如,<shiro:hasPermission name=”admin”>),每个标签都会发起一个查询请求,那么访问一个页面将会向缓存发送大量网络请求,这回给集中缓存组件带来一定的瞬时请求压力,而且,网络查询的效率并不高。采用本地缓存则不存在这些问题。所以,如果在项目中使用了大量shiro标签,那还是采用本次缓存更合适。
- MemoryConstraintCacheManager: 本地缓存。
EhCacheManager:集中式缓存。
CacheManagerAware:用于注入CacheManager
public interface CacheManagerAware {
//注入CacheManager
void setCacheManager(CacheManager cacheManager);
}
- Shiro内部的组件DefaultSecurityManager会自动检测相应的对象(如Realm)是否实现了CacheManagerAware,并自动注入相应的CacheManager。
Realm缓存
- 即权限数据的缓存(其实还有用户身份AuthenticationInfo的缓存,不过用的比较少,暂且忽略),需要设置一个CacheManager来管理缓存,设置方式有两种。
- 设置在SecurityManager中,最终也会设置给CachingRealm,其实真正使用CacheManager的组件也就realm和SessionDAO。
- 其中CachingSecurityManager有CacheManager属性,会把它设置给CachingRealm。
- 推荐:直接设置给CachingRealm。
<bean id="myRealm" class="org.chench.test.shiro.spring.dao.ShiroCacheJdbcRealm">
<property name="dataSource" ref="dataSource"/>
<property name="permissionsLookupEnabled" value="true"/>
<property name="cacheManager" ref="cacheManager" />
</bean>
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager" />
- Shiro提供了几个Realm,它们实现了CacheManagerAware接口,能够实现缓存的一些基础功能。
- CachingRealm: 最基础的实现,实现了CacheManagerAware接口,提供了缓存的一些基础实现。
- AuthenticatingRealm、AuthorizingRealm:分别提供了对AuthenticationInfo和AuthorizationInfo的缓存。
示例
- 测试用例:仿ideaProjects/shiroHelloWorld/chapter6
- 在UserRealm中添加了6个方法用于清除缓存:
//重写以下方法并改为public,否则测试无法调用这些Protected的方法
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
//同时调用以上俩方法,清空两个Info
super.clearCache(principals);
}
public void clearAllCachedAuthorizationInfo(){
getAuthorizationCache().clear();
}
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
- shiro.ini
userRealm=com.github.zhangkaitao.shiro.chapter11.realm.UserRealm
//启用缓存,默认false,看源码默认好像是true。
userRealm.cachingEnabled=true
//启用身份验证缓存,缓存AuthenticationInfo,默认false
userRealm.authenticationCachingEnabled=true
//缓存AuthenticationInfo的缓存名称
userRealm.authenticationCacheName=authenticationCache
//启用授权缓存,默认true
userRealm.authorizationCachingEnabled=true
userRealm.authorizationCacheName=authorizationCache
securityManager.realms=$userRealm
//缓存管理器,此处使用EhCacheManager,即Ehcache实现,需要导入Ehcache的依赖
cacheManager=org.apache.shiro.cache.ehcache.EhCacheManager
cacheManager.cacheManagerConfigFile=classpath:shiro-ehcache.xml
securityManager.cacheManager=$cacheManager
这篇参考文章又说只要设置了CacheManager就会自动开启缓存,实际测试好像也是这样。
Ehcache依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency>
引入这个就不需要之前的ehcache-core依赖了。
ehcache.xml:配置Ehcache缓存,可以将数据存储到磁盘或内存中。
<?xml version="1.0" encoding="UTF-8"?> <ehcache name = "shirocache"> <diskStore path="java.io.tmpdir"/> //数据缓存地址,如,F:/develop/ehcache <cache name="authorizationCache" //缓存名称 maxEntriesLocalHeap="2000" //缓存最大条目数 eternal="false" //对象是否永久有效,true则timeout失效 timeToIdleSeconds="3600" //对象在失效前的闲置时间(单位:s), //仅eternal=false时有效;默认为0,即可闲置时间无穷大。 timeToLiveSeconds="0" //缓存数据的生成时间(单位:s), //介于创建时间和失效时间之间;仅eternal=false有效; //默认为0,即对象存活时间无穷大。 overflowToDisk="false" //内存中对象数量达到maxElementInMemory时, //是否将对象写到磁盘 statistics="true"> </cache> <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> </ehcache>
cache标签其他属性:
- diskSpoolBufferSizeMB:设置diskStore磁盘缓存的缓存区大小,默认30MB。每个Cache都应该有自己的一个缓存区。
- maxElementOnDisk:磁盘最大缓存个数。
- diskPersistent:是否缓存虚拟机重启期数据,默认false。
- diskExpiryThreadIntervalSeconds: 磁盘失效线程运行时间间隔,默认120s。
- memoryStoreEvictionPolicy:达到maxElementInMemory时,Ehcache将会根据此策略去清理内存,默认策略是LRU(最近最少使用),可设为FIFO(先进先出)或LFU(较少使用)。
- clearOnFlush: 内存数量最大时是否清除。
因为测试用例的关系,需要将Ehcache的CacheManager改为使用VM单例模式(不过本例好像没有在哪里修改)
this.manager = new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream());
//改为
this.manager = net.sf.ehcache.CacheManager.create(getCacheManagerConfigFileInputStream());
- 测试:首先登陆成功,此时会缓存相应的AuthenticationInfo;然后修改密码,此时AuthenticationInfo过期;接着需要调用realm的clearCacheAuthenticationInfo()清空之前的AuthenticationInfo,否则下次登录时还会获取那个修改密码前的AuthenticationInfo。
@Test
public void testClearCachedAuthenticationInfo() {
//1、登陆成功即缓存AuthenticationInfo
login(u1.getUsername(), password);
//2、修改了密码则之前保存的AuthenticationInfo变旧
userService.changePassword(u1.getId(), password + "1");
//3、清除之前缓存的AuthenticationInfo,否则下次登录时还会获取到旧的
//AuthenticationInfo
RealmSecurityManager securityManager =
(RealmSecurityManager) SecurityUtils.getSecurityManager();
//获取Realm
UserRealm userRealm = (UserRealm) securityManager.getRealms().iterator().next();
userRealm.clearCachedAuthenticationInfo(subject().getPrincipals());
//4、再次登录,检测到AuthenticationInfo已清空,故重新缓存
login(u1.getUsername(), password + "1");
}
- 无论用户是否正常退出,缓存都将自动清空。
- 如果修改了用户的权限,而用户不退出系统,则修改的权限无法立即生效。需要用户在修改后手动调用clearXxx()清除缓存。
验证中缓存的使用
- 以上测试中,登录后即缓存AuthenticationInfo,实际是AuthenticationRealm类中的getAuthenticationInfo()起作用,首先判断缓存中是否已有该记录,否则调用子类Realm的doGetAuthenticationInfo()查询数据库,并将结果缓存起来,下次就不用查询数据库了。核心代码:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//首先从缓存中获取记录
AuthenticationInfo info = getCachedAuthenticationInfo(token);
//若缓存中无此数据
if (info == null) {
//则从数据库查找
info = doGetAuthenticationInfo(token);
if (token != null && info != null) {
//将记录缓存起来
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
//debug级日志记录
}
if (info != null) {
//匹配密码
assertCredentialsMatch(token, info);
} else {
//debug级日志记录
}
return info;
}
授权中缓存的使用
- 而AuthorizationInfo的缓存则是AuthorizingRealm中的getAuthorizationInfo()在实现,也是先找缓存,没有再调用doGetAuthorizationInfo()去数据库找。
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
//如果登录成功的用户身份信息集合为空,则直接返回null
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
Object key = getAuthorizationCacheKey(principals); //这个集合只有一个key
info = cache.get(key);
}
//如果缓存中找不到AuthorizationInfo
if (info == null) {
//从数据库找
info = doGetAuthorizationInfo(principals);
//并存入缓存
if (info != null && cache != null) {
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info);
}
}
return info;
}
- 如果需要实现自己的缓存,可以考虑自己通过aop机制实现而废弃Shiro的缓存
- 如果要和Spring集成可以考虑使用SpringCacheManagerWrapper,它对Spring Cache进行了包装,转换为Shiro的CacheManager实现类
Session缓存
- 为了使会话能够用上缓存(比如查询会话时先看看缓存中是否有,没有再倒数据库查询),可以先设置SecurityManager的CacheManager属性,再设置SecurityManager的SessionManager属性,那么会自动把配置的CacheManager注入到SessionManager中。
//设置CacheManager
securityManager.cacheManager=$cacheManager
//设置SessionManager
sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
securityManager.sessionManager=$sessionManager
- 若SecurityManager实现了SessionsSecurityManager接口,则会自动判断SessionManager是否实现了CacheManagerAware接口,是则将CacheManager注入给它。然后SessionManager会判断SessionDAO是否实现了CacheManagerAware接口(如继承自CachingSessionDAO),是则会把CacheManager注入给它。
- 对于CachingSessionDAO,可以设置缓存的名称:
sessionDAO=com.github.zhangkaitao.shiro.chapter11.session.dao.MySessionDAO
//默认为此
sessionDAO.activeSessionsCacheName=shiro-activeSessionCache