缓存

  • Shiro提供了类似于Spring的Cache抽象,即它本身不实现cache,但是对cache进行了抽象,方便更换底层cache实现(如,Ehcache,Hazelcast,OSCache,Terracotta,Coherence,GigaSpaces,JBossCache)。
  • Cache接口:
public interface Cache {
    //根据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 keys();

    //获取缓存中所有的value
    public Collection values();
}
  • CacheManager接口:从接口方法来看就是用来获取cache的。
public interface CacheManager {
    //根据缓存名字获取一个Cache实例,不存在则新建一个
    public  Cache getCache(String name) throws CacheException;
}

Shiro第十一章-cache缓存_第1张图片

  • 从上面来看,我们定义缓存要做两件事,一是实现Cache接口(shiro-ehcache.xml),二是实现CacheManager接口(shiro.ini中注册EhCacheManager的bean)。

  • Shiro数据的缓存方式分为两类,一是将数据存储到本地,一是存储到集中式存储中间件,如,redis或Memcached。若使用后者,当页面使用了大量shiro标签时(如,),每个标签都会发起一个查询请求,那么访问一个页面将会向缓存发送大量网络请求,这回给集中缓存组件带来一定的瞬时请求压力,而且,网络查询的效率并不高。采用本地缓存则不存在这些问题。所以,如果在项目中使用了大量shiro标签,那还是采用本次缓存更合适。

  • MemoryConstraintCacheManager: 本地缓存。
  • EhCacheManager:集中式缓存。

  • CacheManagerAware:用于注入CacheManager

public interface CacheManagerAware {
    //注入CacheManager
    void setCacheManager(CacheManager cacheManager);
}
  • Shiro内部的组件DefaultSecurityManager会自动检测相应的对象(如Realm)是否实现了CacheManagerAware,并自动注入相应的CacheManager。

Realm缓存

  • 即权限数据的缓存(其实还有用户身份AuthenticationInfo的缓存,不过用的比较少,暂且忽略),需要设置一个CacheManager来管理缓存,设置方式有两种。
  1. 设置在SecurityManager中,最终也会设置给CachingRealm,其实真正使用CacheManager的组件也就realm和SessionDAO。
Shiro第十一章-cache缓存_第2张图片
  • 其中CachingSecurityManager有CacheManager属性,会把它设置给CachingRealm。
  1. 推荐:直接设置给CachingRealm。
Shiro第十一章-cache缓存_第3张图片
Shiro第十一章-cache缓存_第4张图片

    
    
    



  • 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依赖

    
        org.apache.shiro
        shiro-ehcache
        1.2.2
    
    
  • 引入这个就不需要之前的ehcache-core依赖了。

  • ehcache.xml:配置Ehcache缓存,可以将数据存储到磁盘或内存中。

    
    
         //数据缓存地址,如,F:/develop/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 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
  • 《跟我学Shiro》第十一章
  • 参考文章