当前位置 博文首页 > 快乐的小三菊的博客:springboot + shiro 配置 ehcache 缓存

    快乐的小三菊的博客:springboot + shiro 配置 ehcache 缓存

    作者:[db:作者] 时间:2021-07-28 20:47

    背景:

    ? ? ? ?shiro 是支持缓存功能的,它可以对用户的授权数据认证数据进行缓存,使得用户不必每次认证或授权时都去查询数据库,今天讲下如何将 ehcache 缓存整合到 shiro 中,后续再讲下如何将 redis 整合到 shiro 中。

    添加依赖:

        <dependency>
    		<groupId>org.apache.shiro</groupId>
    		<artifactId>shiro-ehcache</artifactId>
    		<version>1.6.0</version>
    	</dependency>

    配置ShiroConfig:

    ? ? ? ?首先在 ShiroConfig 中添加 shiro 的缓存管理器 EhCacheManager ,并将缓存管理器添加到 SecurityManager 中,最后在?CustomRealm 中配置开启缓存,如下所示:

        /**
    	 * shiro缓存管理器;
    	 * 需要添加到securityManager中
    	 * @return
    	 */
    	@Bean
    	public EhCacheManager ehCacheManager(){
    	    EhCacheManager cacheManager = new EhCacheManager();
    	    cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
    	    return cacheManager;
    	}
        @Bean
    	public SecurityManager securityManager() {
    		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    		securityManager.setRealm(myShiroRealm());
    		// 将 CookieRememberMeManager 注入到 SecurityManager 中,否则不会生效
    		securityManager.setRememberMeManager(rememberMeManager());
    		// 将 sessionManager 注入到 SecurityManager 中,否则不会生效
    		securityManager.setSessionManager(sessionManager());
    		// 将 EhCacheManager 注入到 SecurityManager 中,否则不会生效
    		securityManager.setCacheManager(ehCacheManager());
    		return securityManager;
    	}
        @Bean
    	public CustomRealm myShiroRealm() {
    		CustomRealm customRealm = new CustomRealm();
    		// 告诉realm,使用credentialsMatcher加密算法类来验证密文
    		customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
    		/* 开启支持缓存,需要配置如下几个参数 */
    		customRealm.setCachingEnabled(true);
    	    // 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
    		customRealm.setAuthenticationCachingEnabled(true);
    	    // 缓存AuthenticationInfo信息的缓存名称 在 ehcache-shiro.xml 中有对应缓存的配置
    		customRealm.setAuthenticationCacheName("authenticationCache");
    	    // 启用授权缓存,即缓存AuthorizationInfo信息,默认false
    		customRealm.setAuthorizationCachingEnabled(true);
    	    // 缓存AuthorizationInfo 信息的缓存名称  在 ehcache-shiro.xml 中有对应缓存的配置
    		customRealm.setAuthorizationCacheName("authorizationCache");
    		return customRealm;
    	}

    添加配置文件:

    ? ? ? ?需要在 resources 文件夹下添加 ehcache-shiro.xml 的配置文件,内容如下所示:

    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache name="es">
    
        <!--
            缓存对象存放路径
            java.io.tmpdir:默认的临时文件存放路径。
            user.home:用户的主目录。
            user.dir:用户的当前工作目录,即当前程序所对应的工作路径。
            其它通过命令行指定的系统属性,如“java –DdiskStore.path=D:\\abc ……”。
        -->
        <diskStore path="java.io.tmpdir"/>
    
        <!--
    
           name:缓存名称。
           maxElementsOnDisk:硬盘最大缓存个数。0表示不限制
           maxEntriesLocalHeap:指定允许在内存中存放元素的最大数量,0表示不限制。
           maxBytesLocalDisk:指定当前缓存能够使用的硬盘的最大字节数,其值可以是数字加单位,单位可以是K、M或者G,不区分大小写,
                              如:30G。当在CacheManager级别指定了该属性后,Cache级别也可以用百分比来表示,
                              如:60%,表示最多使用CacheManager级别指定硬盘容量的60%。该属性也可以在运行期指定。当指定了该属性后会隐式的使当前Cache的overflowToDisk为true。
           maxEntriesInCache:指定缓存中允许存放元素的最大数量。这个属性也可以在运行期动态修改。但是这个属性只对Terracotta分布式缓存有用。
           maxBytesLocalHeap:指定当前缓存能够使用的堆内存的最大字节数,其值的设置规则跟maxBytesLocalDisk是一样的。
           maxBytesLocalOffHeap:指定当前Cache允许使用的非堆内存的最大字节数。当指定了该属性后,会使当前Cache的overflowToOffHeap的值变为true,
                                 如果我们需要关闭overflowToOffHeap,那么我们需要显示的指定overflowToOffHeap的值为false。
           overflowToDisk:boolean类型,默认为false。当内存里面的缓存已经达到预设的上限时是否允许将按驱除策略驱除的元素保存在硬盘上,默认是LRU(最近最少使用)。
                          当指定为false的时候表示缓存信息不会保存到磁盘上,只会保存在内存中。
                          该属性现在已经废弃,推荐使用cache元素的子元素persistence来代替,如:<persistence strategy=”localTempSwap”/>。
           diskSpoolBufferSizeMB:当往磁盘上写入缓存信息时缓冲区的大小,单位是MB,默认是30。
           overflowToOffHeap:boolean类型,默认为false。表示是否允许Cache使用非堆内存进行存储,非堆内存是不受Java GC影响的。该属性只对企业版Ehcache有用。
           copyOnRead:当指定该属性为true时,我们在从Cache中读数据时取到的是Cache中对应元素的一个copy副本,而不是对应的一个引用。默认为false。
           copyOnWrite:当指定该属性为true时,我们在往Cache中写入数据时用的是原对象的一个copy副本,而不是对应的一个引用。默认为false。
           timeToIdleSeconds:单位是秒,表示一个元素所允许闲置的最大时间,也就是说一个元素在不被请求的情况下允许在缓存中待的最大时间。默认是0,表示不限制。
           timeToLiveSeconds:单位是秒,表示无论一个元素闲置与否,其允许在Cache中存在的最大时间。默认是0,表示不限制。
           eternal:boolean类型,表示是否永恒,默认为false。如果设为true,将忽略timeToIdleSeconds和timeToLiveSeconds,Cache内的元素永远都不会过期,也就不会因为元素的过期而被清除了。
           diskExpiryThreadIntervalSeconds :单位是秒,表示多久检查元素是否过期的线程多久运行一次,默认是120秒。
           clearOnFlush:boolean类型。表示在调用Cache的flush方法时是否要清空MemoryStore。默认为true。
           diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
           maxElementsInMemory:缓存最大数目
           memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
                memoryStoreEvictionPolicy:
                   Ehcache的三种清空策略;
                   FIFO,first in first out,这个是大家最熟的,先进先出。
                   LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
                   LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
        -->
        <defaultCache
                maxElementsInMemory="10000"
                eternal="false"
                timeToIdleSeconds="0"
                timeToLiveSeconds="0"
                overflowToDisk="false"
                diskPersistent="false"
                diskExpiryThreadIntervalSeconds="120"
        />
    
        <!-- 授权缓存 -->
        <cache name="authorizationCache"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="0"
               timeToLiveSeconds="0"
               overflowToDisk="false"
               statistics="true">
        </cache>
    
        <!-- 认证缓存 -->
        <cache name="authenticationCache"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="0"
               timeToLiveSeconds="0"
               overflowToDisk="false"
               statistics="true">
        </cache>
    
    </ehcache>

    配置日志输出验证:? ? ??

    ? ? ? ?在 CustomRealm 类的 doGetAuthorizationInfo() 方法中添加打印,或者直接看控制台 sql 打印也行,如下所示:

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    		System.out.println("开始查询数据库");
    		// 获取登录用户名
    		String name = (String)principalCollection.getPrimaryPrincipal();
    		// 查询用户名称
    		User user = userService.selectByUserName(name);
    		// 添加角色和权限
    		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    		List<String> roleNameList = new ArrayList<>();
    		List<String> permissionNameList = new ArrayList<>();
    
    		for (Role role : user.getRoles()) {
    			roleNameList.add(role.getRoleName());
    			for (Permission permission : role.getPermissions()) {
    				permissionNameList.add(role.getRoleName()+":"+permission.getPermissionName());
    			}
    		}
    		// 添加角色
    		simpleAuthorizationInfo.addRoles(roleNameList);
    		// 添加权限
    		simpleAuthorizationInfo.addStringPermissions(permissionNameList);
    		System.out.println("查询数据库结束");
    		return simpleAuthorizationInfo;
    	}

    ? ? ? ?启动项目,使用 zhangsan 账号登录,第一次访问,查看控制台,有日志输出,如下所示,下面只截取了一部分的日志输出,再次访问将不再打印输出,证明缓存生效了。上面的缓存配置时间配置为永久,请根据需求自己更改值来进行测试。?在?CustomRealm 中对用户信息角色信息权限信息的查询,如果需要添加缓存请自行处理。

    更改权限:

    ? ? ? ?当用户的权限发生改变时我们该如何处理?上面的代码中已经启用了缓存,第一次请求走数据库查询,后续请求将直接查询 ehcache 缓存,假如这个时候在权限控制台分配了某个权限给某个角色,那么拥有这个角色的所有用户在下次请求之前都需要从数据库查询最新的权限信息。所以我们需要清理缓存。

    ? ? ? ?首先在?CustomRealm 中添加下面的方法用于清理缓存,如下所示:

    /**
    	 * 重写方法,清除当前用户的的 授权缓存
    	 * @param principals
    	 */
    	@Override
    	public void clearCachedAuthorizationInfo(PrincipalCollection principal) {
    		 super.clearCachedAuthorizationInfo(principal);
    	}
    	/**
    	 * 重写方法,清除当前用户的 认证缓存
    	 * @param principals
    	 */
    	@Override
    	public void clearCachedAuthenticationInfo(PrincipalCollection principal) {
    		super.clearCachedAuthenticationInfo(principal);
    	}
    
    	/**
    	 *  重写方法,清除当前用户的 认证缓存和授权缓存
    	 * */
    	@Override
    	public void clearCache(PrincipalCollection principals) {
    		super.clearCache(principals);
    	}
    
    	/**
    	 * 自定义方法:清除所有用户的 授权缓存
    	 */
    	public void clearAllCachedAuthorizationInfo() {
    		getAuthorizationCache().clear();
    	}
    
    	/**
    	 * 自定义方法:清除所有用户的 认证缓存
    	 */
    	public void clearAllCachedAuthenticationInfo() {
    		getAuthenticationCache().clear();
    	}
    
    	/**
    	 * 自定义方法:清除所有用户的  认证缓存  和 授权缓存
    	 */
    	public void clearAllCache() {
    		clearAllCachedAuthenticationInfo();
    		clearAllCachedAuthorizationInfo();
    	}

    ? ? ? ? 然后在?ShiroConfig 中添加 MethodInvokingFactoryBean ,代码如下所示:

        /**
    	 * 让某个实例的某个方法的返回值注入为Bean的实例
    	 * Spring静态注入
    	 * @return
    	 */
    	@Bean
    	public MethodInvokingFactoryBean getMethodInvokingFactoryBean(){
    	    MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean();
    	    factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
    	    factoryBean.setArguments(new Object[]{securityManager()});
    	    return factoryBean;
    	}

    测试:

    ? ? ? ?在 LoginController 中添加如下方法用于清除缓存,在添加或者删除权限的时候,都需要清除缓存。

    /**
         * 给admin用户添加 userInfo:del 权限
         * @param model
         * @return
         */
        @RequestMapping(value = "/addRoleIds",method = RequestMethod.GET)
        @ResponseBody
        public String addPermission(String userName) {
            //在sys_role_permission 表中  将 删除的权限 关联到admin用户所在的角色
        	User user = new User();
        	user.setUserName(userName);
        	user.setRoleIds("1,2");
        	userService.updateRoleIds(user);
    
            //添加成功之后 清除缓存
            DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager();
            CustomRealm shiroRealm = (CustomRealm) securityManager.getRealms().iterator().next();
            // 清除所有用户权限相关的缓存
            // shiroRealm.clearAllCache();
            // 清除当前登录用户的缓存
            shiroRealm.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
            return "给admin用户添加 userInfo:del 权限成功";
        }
        /**
         * 删除admin用户 userInfo:del 权限
         * @param model
         * @return
         */
        @RequestMapping(value = "/delRoleIds",method = RequestMethod.GET)
        @ResponseBody
        public String delPermission(String userName) {
            //在sys_role_permission 表中  将 删除的权限 关联到admin用户所在的角色
        	User user = new User();
        	user.setUserName(userName);
        	user.setRoleIds("1");
        	userService.updateRoleIds(user);
        	
            //添加成功之后 清除缓存
            DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager();
            CustomRealm shiroRealm = (CustomRealm) securityManager.getRealms().iterator().next();
            // 清除所有用户权限相关的缓存
            // shiroRealm.clearAllCache();
            // 清除当前登录用户的缓存
            shiroRealm.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
            return "删除admin用户userInfo:del 权限成功";
    
        }

    ? ? ? ?1、用两个浏览器分别用不同的用户登录到系统中,然后刷新界面,我们发现,无论怎么刷新,都不用有日志输出,因为此时查询的时缓存。

    ? ? ? ?2、点击界面的新增按钮,触发后台的 addRoleIds 方法,然后再刷新界面,我们发现此时后台有日志输出,即查询的数据是数据库里面的数据。

    ? ? ? ?3、点击界面的删除按钮,触发后台的 delRoleIds 方法,然后再刷新界面,我们发现此时后台有日志输出,即查询的数据是数据库里面的数据。

    cs