SpringBoot整合Shiro(三)
整合shiro-ehcache缓存
我们最后有两个问题,其中关于无权限页面的问题已经解决了,还有一个问题就是每次访问服务,都会去查询数据库,真实的情况是权限并不会经常出现变化,所以最好的办法就是做缓存处理,如果只是公司后台运营管理系统,可能只启动一个单节点就够了,这个时候我们就可以用到ehcache缓存。
前置工作 修改RoleService
增加delPermission,addPermission 方法
1 2 3 4 5 6 7 8 public interface RoleService { Set<Role> findRolesByUserId (Integer uid) ; void delPermission (int roleId, int permissionId) ; void addPermission (int i, int i1) ; }
RoleServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class RoleServiceImpl implements RoleService { @Autowired private RoleMapper roleMapper; @Override public Set<Role> findRolesByUserId (Integer uid) { return roleMapper.findRolesByUserId(uid); } @Override public void delPermission (int roleId, int permissionId) { roleMapper.delPermission(roleId,permissionId); } @Override public void addPermission (int roleId, int permissionId) { roleMapper.addPermission(roleId,permissionId); } }
修改RoleMapper 1 2 3 4 5 6 7 8 9 @Mapper public interface RoleMapper { Set<Role> findRolesByUserId (@Param("uid") Integer uid) ; void delPermission (@Param("roleId") Integer roleId, @Param("permissionId") Integer permissionId) ; void addPermission (@Param("roleId") Integer roleId, @Param("permissionId") Integer permissionId) ; }
RoleMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.demo.mapper.RoleMapper" > <select id ="findRolesByUserId" resultType ="com.demo.entity.Role" > SELECT r.* from sys_role r LEFT JOIN sys_user_role ur on r.id = ur.role_id where ur.uid = #{uid} </select > <insert id ="addPermission" > INSERT INTO sys_role_permission (role_id, permission_id) VALUES (#{roleId},#{permissionId}) </insert > <delete id ="delPermission" > DELETE FROM sys_role_permission WHERE role_id=#{roleId} and permission_id=#{permissionId} </delete > </mapper >
pom添加依赖 1 2 3 4 5 <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-ehcache</artifactId > <version > 1.5.2</version > </dependency >
修改ShiroConfig
在ShiroConfig中添加缓存管理器
添加shiro缓存管理器 1 2 3 4 5 6 7 8 9 10 11 12 @Bean public EhCacheManager ehCacheManager () { EhCacheManager cacheManager = new EhCacheManager (); cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml" ); return cacheManager; }
添加Spring静态注入 1 2 3 4 5 6 7 8 9 10 11 12 13 @Bean public MethodInvokingFactoryBean getMethodInvokingFactoryBean () { MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean (); factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager" ); factoryBean.setArguments(new Object []{securityManager()}); return factoryBean; }
修改安全事务管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Bean(name = "securityManager") public SecurityManager securityManager () { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(shiroRealm()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setCacheManager(ehCacheManager()); return securityManager; }
重写shiroRealm方法
重写shiroRealm开启Ehcache缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public ShiroRealm shiroRealm () { ShiroRealm shiroRealm = new ShiroRealm (); shiroRealm.setCachingEnabled(true ); shiroRealm.setAuthenticationCachingEnabled(true ); shiroRealm.setAuthenticationCacheName("authenticationCache" ); shiroRealm.setAuthorizationCachingEnabled(true ); shiroRealm.setAuthorizationCacheName("authorizationCache" ); return shiroRealm; }
配置ehcache配置文件
在config下添加ehcache-shiro.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 <?xml version="1.0" encoding="UTF-8" ?> <ehcache name ="es" > <diskStore path ="java.io.tmpdir" /> <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 >
在ShiroRealm的doGetAuthorizationInfo方法中添加日志,或者直接看控制台sql打印也行。 启动项目,使用admin登录,访问http://localhost:8080/userInfo/view 第一次访问,查看控制台,有sql查询,再次访问将不再打印查询数据库的日志,证明缓存生效了。上面的缓存配置时间配置为永久,请根据需求自己更改值来进行测试。 关于在ShiroRealm中对 用户信息 角色信息 权限信息的查询,如果需要添加缓存 请自行处理。
缓存更新 如果用户的权限发生改变怎么办 上面已经启用了缓存,第一次请求走数据库查询,后续请求将直接查询ehcache缓存,假如这个时候在权限控制台分配了某个权限给某个角色,那么拥有这个角色的所有用户在下次请求之前都需要从数据库查询最新的权限信息。下面开始进行在权限发生改变时,该如何做:
清理缓存
在ShiroRealm类中添加以下方法(清理缓存)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class ShiroRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; ... @Override public void clearCachedAuthorizationInfo (PrincipalCollection principals) { super .clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo (PrincipalCollection principals) { super .clearCachedAuthenticationInfo(principals); } @Override public void clearCache (PrincipalCollection principals) { super .clearCache(principals); } public void clearAllCachedAuthorizationInfo () { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo () { getAuthenticationCache().clear(); } public void clearAllCache () { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }
修改UserController
在UserController中添加下面两个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @RestController @RequestMapping("userInfo") public class UserController { @Autowired private UserService userService; @Autowired private RoleService roleService; ..... @RequestMapping(value = "/addPermission",method = RequestMethod.GET) @ResponseBody public String addPermission (Model model) { roleService.addPermission(1 ,3 ); DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager(); ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next(); shiroRealm.clearAllCache(); return "给admin用户添加 userInfo:del 权限成功" ; } @RequestMapping(value = "/delPermission",method = RequestMethod.GET) @ResponseBody public String delPermission (Model model) { roleService.delPermission(1 ,3 ); DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next(); shiroRealm.clearAllCache(); return "删除admin用户userInfo:del 权限成功" ; } }
注意:在添加权限 或者 删除权限之后 都有调用shiroRealm.clearAllCache();来清除所有的缓存。
测试步骤:
两个浏览器: 一个谷歌浏览器登录 admin账户,另一个360浏览器登录 test账户,每个账户登录跳转到idnex页面之后 ,刷新两下,发现之后无论怎么刷新都不再打印查询数据库的sql
在谷歌浏览器地址调用添加权限的方法http://localhost:8080/userInfo/addPermission 显示添加完成,然后再次访问http://localhost:8080/index 页面上已经显示刚添加的权限,查看日志发现走数据库查询了最新的权限信息
这个时候在360浏览器刷新http://localhost:8080/index 查看控制台日志发现test的用户也有走数据库查询权限信息
shiro会话管理 Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Tomcat),不管是J2SE还是J2EE环境都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关的集群,失效/过期支持,对Web的透明支持,SSO单点登录的支持等特性。即直接使用 Shiro 的会话管理可以直接替换如 Web 容器的会话管理。
shiro中的session特性
基于POJO/J2SE:shiro中session相关的类都是基于接口实现的简单的java对象(POJO),兼容所有java对象的配置方式,扩展也更方便,完全可以定制自己的会话管理功能 。
简单灵活的会话存储/持久化:因为shiro中的session对象是基于简单的java对象的,所以你可以将session存储在任何地方,例如,文件,各种数据库,内存中等。
容器无关的集群功能:shiro中的session可以很容易的集成第三方的缓存产品完成集群的功能。例如,Ehcache + Terracotta, Coherence, GigaSpaces等。你可以很容易的实现会话集群而无需关注底层的容器实现。
异构客户端的访问:可以实现web中的session和非web项目中的session共享。
会话事件监听:提供对对session整个生命周期的监听。
保存主机地址:在会话开始session会存用户的ip地址和主机名,以此可以判断用户的位置。
会话失效/过期的支持:用户长时间处于不活跃状态可以使会话过期,调用touch()方法,可以主动更新最后访问时间,让会话处于活跃状态。
透明的Web支持:shiro全面支持Servlet 2.5中的session规范。这意味着你可以将你现有的web程序改为shiro会话,而无需修改代码。
单点登录的支持:shiro session基于普通java对象,使得它更容易存储和共享,可以实现跨应用程序共享。可以根据共享的会话,来保证认证状态到另一个程序。从而实现单点登录。
会话相关API 获取Session的方式 1 2 Subject subject = SecurityUtils.getSubject();Session session = subject.getSession();
与web中的 HttpServletRequest.getSession(boolean create) 类似! Subject.getSession(true)。即如果当前没有创建session对象会创建一个; Subject.getSession(false),如果当前没有创建session对象则返回null。
Session相关API
返回值
方法名
描述
Object
getAttribute(Object key)
根据key标识返回绑定到session的对象
Collection
getAttributeKeys()
获取在session中存储的所有的key
String
getHost()
获取当前主机ip地址,如果未知,返回null
Serializable
getId()
获取session的唯一id
Date
getLastAccessTime()
获取最后的访问时间
Date
getStartTimestamp()
获取session的启动时间
long
getTimeout()
获取session失效时间,单位毫秒
void
setTimeout(long maxIdleTimeInMillis)
设置session的失效时间
Object
removeAttribute(Object key)
通过key移除session中绑定的对象
void
setAttribute(Object key, Object value)
设置session会话属性
void
stop()
销毁会话
void
touch()
更新会话最后访问时间
会话管理器
会话管理器管理着应用中所有Subject的会话的创建、维护、删除、失效、验证等工作。是Shiro的核心组件,顶层组件SecurityManager直接继承了SessionManager,且提供了SessionsSecurityManager实现直接把会话管理委托给相应的SessionManager,DefaultSecurityManager及DefaultWebSecurityManager默认SecurityManager都继承了SessionsSecurityManager。
SecurityManager提供了如下接口:
另外用于Web环境的WebSessionManager又提供了如下接口:
boolean isServletContainerSessions();//是否使用Servlet容器的会话
Shiro还提供了ValidatingSessionManager用于验资并过期会话:
void validateSessions();//验证所有会话是否过期
Shiro提供了三个默认实现
DefaultSessionManager DefaultSecurityManager使用的默认实现,用于JavaSE环境;
ServletContainerSessionManager DefaultWebSecurityManager使用的默认实现,用于Web环境,其直接使用Servlet容器的会话;
DefaultWebSessionManager 用于Web环境的实现,可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了Servlet容器的会话管理。
shiro配置会话管理配置 SessionListener
创建session监听类,实现SessionListener接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class ShiroSessionListener implements SessionListener { private final AtomicInteger sessionCount = new AtomicInteger (0 ); @Override public void onStart (Session session) { sessionCount.incrementAndGet(); } @Override public void onStop (Session session) { sessionCount.decrementAndGet(); } @Override public void onExpiration (Session session) { sessionCount.decrementAndGet(); } public AtomicInteger getSessionCount () { return sessionCount; } }
配置ShiroConfig 配置session监听 1 2 3 4 5 6 7 8 9 10 @Bean("sessionListener") public ShiroSessionListener sessionListener () { ShiroSessionListener sessionListener = new ShiroSessionListener (); return sessionListener; }
配置会话ID生成器 1 2 3 4 5 6 7 8 9 @Bean public SessionIdGenerator sessionIdGenerator () { return new JavaUuidSessionIdGenerator (); }
配置sessionDAO
SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
MemorySessionDAO 直接在内存中进行会话维护
EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Bean public SessionDAO sessionDAO () { EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO (); enterpriseCacheSessionDAO.setCacheManager(ehCacheManager()); enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache" ); enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator()); return enterpriseCacheSessionDAO; }
配置sessionId的Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Bean("sessionIdCookie") public SimpleCookie sessionIdCookie () { SimpleCookie simpleCookie = new SimpleCookie ("session_id" ); simpleCookie.setHttpOnly(true ); simpleCookie.setPath("/" ); simpleCookie.setMaxAge(-1 ); return simpleCookie; }
配置session管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Bean("sessionManager") public SessionManager sessionManager () { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager (); Collection<SessionListener> listeners = new ArrayList <SessionListener>(); listeners.add(sessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setSessionIdCookie(sessionIdCookie()); sessionManager.setSessionDAO(sessionDAO()); sessionManager.setCacheManager(ehCacheManager()); sessionManager.setGlobalSessionTimeout(1800000 ); sessionManager.setDeleteInvalidSessions(true ); sessionManager.setSessionValidationSchedulerEnabled(true ); sessionManager.setSessionValidationInterval(3600000 ); sessionManager.setSessionIdUrlRewritingEnabled(false ); return sessionManager; }
注意:这里的SessionIdCookie 是新建的一个SimpleCookie对象,不是之前整合记住我的那个rememberMeCookie 如果配错了,就会出现session经典问题:每次请求都是一个新的session 并且后台报以下异常,解析的时候报错.因为记住我cookie是加密的用户信息,所以报解密错误
1 org.apache.shiro.crypto.CryptoException: Unable to execute 'doFinal' with cipher instance [javax.crypto.Cipher@461df537].
配置清理孤立session
以上整合会话管理,还有一个问题: 如果用户如果不点注销,直接关闭浏览器,不能够进行session的清空处理,所以为了防止这样的问题,还需要增加有一个会话的验证调度。
1 2 3 4 5 6 7 8 9 10 sessionManager.setGlobalSessionTimeout(1800000 ); sessionManager.setDeleteInvalidSessions(true ); sessionManager.setSessionValidationSchedulerEnabled(true ); sessionManager.setSessionValidationInterval(3600000 );
取消URL的JSESSIONID
shiro取消url上面的JSESSIONID
1 2 sessionManager.setSessionIdUrlRewritingEnabled(false );
修改SecurityManager
将session管理器交给SecurityManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean(name = "securityManager") public SecurityManager securityManager () { ... securityManager.setSessionManager(sessionManager()); return securityManager; }
配置Ehcache缓存
ehcache-shiro.xml添加session缓存属性
1 2 3 4 5 6 7 8 9 <cache name ="shiro-activeSessionCache" maxEntriesLocalHeap ="2000" eternal ="false" timeToIdleSeconds ="0" timeToLiveSeconds ="0" overflowToDisk ="false" statistics ="true" > </cache >
注意:
这里一定要注意缓存的设置过期时间,还有setGlobalSessionTimeout
的值,任一个时间设置的比较短,session就会从ehcache中清除,到时候就会报There is no session with id [2e9e317f-7575-4bb0-98c4-3e6e5d2578f5]
启动测试
配置完成之后启动测试,登陆的时候点击 rememberMe 查看cookie 可以看到一个sessionId 和 一个记住我cookie