缓存

  • 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来管理缓存,设置方式有两种。
  1. 设置在SecurityManager中,最终也会设置给CachingRealm,其实真正使用CacheManager的组件也就realm和SessionDAO。
  • 其中CachingSecurityManager有CacheManager属性,会把它设置给CachingRealm。
  1. 推荐:直接设置给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;
} 

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