说在前面 (原文连接: https://blog.csdn.net/qq_34021712/article/details/80774649)
原本的整合过程是顺着博客的顺序来的,越往下,集成的越多,因为以前是使用ehcache缓存,如今改成redis,限制登陆人数 以及 限制登陆次数等 都须要改动,本篇为了简单,目前先将这两个功能下线,配置暂时是注销的,原类保存,在下篇博客中改。
还有以前是使用SessionListener监听session建立来统计在线人数,在本篇中也将改成统计redis中的key数目。
若是是单机,使用ehcache是最快的,项目通常都不是单节点,为了方便以后使用sso单点登陆,以及多节点部署,因此使用shiro整合redis。这里有一个开源项目,git地址为:https://github.com/alexxiyang/shiro-redis 在此感谢做者无私奉献。javascript
shiro用redis实现缓存须要重写cache、cacheManager、SessionDAO和初始化redis配置。css
<!-- 整合shiro框架 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!-- shiro-thymeleaf 2.0.0--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>1.2.1</version> </dependency> <!-- shiro-redis --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.1.0</version> </dependency>
package com.springboot.test.shiro.config; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import com.springboot.test.shiro.config.shiro.*; import org.apache.shiro.codec.Base64; import org.apache.shiro.session.SessionListener; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.session.mgt.eis.SessionIdGenerator; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.web.servlet.ErrorPage; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import javax.servlet.Filter; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Properties; /** * @author: wangsaichao * @date: 2018/5/10 * @description: Shiro配置 */ @Configuration public class ShiroConfig { /** * ShiroFilterFactoryBean 处理拦截资源文件问题。 * 注意:初始化ShiroFilterFactoryBean的时候须要注入:SecurityManager * Web应用中,Shiro可控制的Web请求必须通过Shiro主过滤器的拦截 * @param securityManager * @return */ @Bean(name = "shirFilter") public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //必须设置 SecurityManager,Shiro的核心安全接口 shiroFilterFactoryBean.setSecurityManager(securityManager); //这里的/login是后台的接口名,非页面,若是不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/"); //这里的/index是后台的接口名,非页面,登陆成功后要跳转的连接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未受权界面,该配置无效,并不会进行页面跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); //自定义拦截器限制并发人数,参考博客: LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>(); //限制同一账号同时在线的个数 //filtersMap.put("kickout", kickoutSessionControlFilter()); //统计登陆人数 shiroFilterFactoryBean.setFilters(filtersMap); // 配置访问权限 必须是LinkedHashMap,由于它必须保证有序 // 过滤链定义,从上向下顺序执行,通常将 /**放在最为下边 --> : 这是一个坑,一不当心代码就很差使了 LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //配置不登陆能够访问的资源,anon 表示资源均可以匿名访问 //配置记住我或认证经过能够访问的地址 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/druid/**", "anon"); //解锁用户专用 测试用的 filterChainDefinitionMap.put("/unlockAccount","anon"); filterChainDefinitionMap.put("/Captcha.jpg","anon"); //logout是shiro提供的过滤器 filterChainDefinitionMap.put("/logout", "logout"); //此时访问/user/delete须要delete权限,在自定义Realm中为用户受权。 //filterChainDefinitionMap.put("/user/delete", "perms[\"user:delete\"]"); //其余资源都须要认证 authc 表示须要认证才能进行访问 user表示配置记住我或认证经过能够访问的地址 //若是开启限制同一帐号登陆,改成 .put("/**", "kickout,user"); filterChainDefinitionMap.put("/**", "user"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 配置核心安全事务管理器 * @return */ @Bean(name="securityManager") public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //设置自定义realm. securityManager.setRealm(shiroRealm()); //配置记住我 securityManager.setRememberMeManager(rememberMeManager()); //配置redis缓存 securityManager.setCacheManager(cacheManager()); //配置自定义session管理,使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 配置Shiro生命周期处理器 * @return */ @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 身份认证realm; (这个须要本身写,帐号密码校验;权限等) * @return */ @Bean public ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setCachingEnabled(true); //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false shiroRealm.setAuthenticationCachingEnabled(true); //缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置 shiroRealm.setAuthenticationCacheName("authenticationCache"); //启用受权缓存,即缓存AuthorizationInfo信息,默认false shiroRealm.setAuthorizationCachingEnabled(true); //缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置 shiroRealm.setAuthorizationCacheName("authorizationCache"); //配置自定义密码比较器 //shiroRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher()); return shiroRealm; } /** * 必须(thymeleaf页面使用shiro标签控制按钮是否显示) * 未引入thymeleaf包,Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect * @return */ @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } /** * 开启shiro 注解模式 * 能够在controller中的方法前加上注解 * 如 @RequiresPermissions("userInfo:add") * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 解决: 无权限页面不跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized") 无效 * shiro的源代码ShiroFilterFactoryBean.Java定义的filter必须知足filter instanceof AuthorizationFilter, * 只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter, * 因此unauthorizedUrl设置后页面不跳转 Shiro注解模式下,登陆失败与没有权限都是经过抛出异常。 * 而且默认并无去处理或者捕获这些异常。在SpringMVC下须要配置捕获相应异常来通知用户信息 * @return */ @Bean public SimpleMappingExceptionResolver simpleMappingExceptionResolver() { SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver(); Properties properties=new Properties(); //这里的 /unauthorized 是页面,不是访问的路径 properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized"); properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/unauthorized"); simpleMappingExceptionResolver.setExceptionMappings(properties); return simpleMappingExceptionResolver; } /** * 解决spring-boot Whitelabel Error Page * @return */ @Bean public EmbeddedServletContainerCustomizer containerCustomizer() { return new EmbeddedServletContainerCustomizer() { @Override public void customize(ConfigurableEmbeddedServletContainer container) { ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthorized.html"); ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404.html"); ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html"); container.addErrorPages(error401Page, error404Page, error500Page); } }; } /** * cookie对象;会话Cookie模板 ,默认为: JSESSIONID 问题: 与SERVLET容器名冲突,从新定义为sid或rememberMe,自定义 * @return */ @Bean public SimpleCookie rememberMeCookie(){ //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); //setcookie的httponly属性若是设为true的话,会增长对xss防御的安全系数。它有如下特色: //setcookie()的第七个参数 //设为true后,只能经过http访问,javascript没法访问 //防止xss读取cookie simpleCookie.setHttpOnly(true); simpleCookie.setPath("/"); //<!-- 记住我cookie生效时间30天 ,单位秒;--> simpleCookie.setMaxAge(2592000); return simpleCookie; } /** * cookie管理对象;记住我功能,rememberMe管理器 * @return */ @Bean public CookieRememberMeManager rememberMeManager(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); //rememberMe cookie加密的密钥 建议每一个项目都不同 默认AES算法 密钥长度(128 256 512 位) cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; } /** * FormAuthenticationFilter 过滤器 过滤记住我 * @return */ @Bean public FormAuthenticationFilter formAuthenticationFilter(){ FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter(); //对应前端的checkbox的name = rememberMe formAuthenticationFilter.setRememberMeParam("rememberMe"); return formAuthenticationFilter; } /** * shiro缓存管理器; * 须要添加到securityManager中 * @return */ @Bean public RedisCacheManager cacheManager(){ RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); //redis中针对不一样用户缓存 redisCacheManager.setPrincipalIdFieldName("username"); //用户权限信息缓存时间 redisCacheManager.setExpire(200000); return redisCacheManager; } /** * 让某个实例的某个方法的返回值注入为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; } /** * 配置session监听 * @return */ @Bean("sessionListener") public ShiroSessionListener sessionListener(){ ShiroSessionListener sessionListener = new ShiroSessionListener(); return sessionListener; } /** * 配置会话ID生成器 * @return */ @Bean public SessionIdGenerator sessionIdGenerator() { return new JavaUuidSessionIdGenerator(); } @Bean public RedisManager redisManager(){ RedisManager redisManager = new RedisManager(); redisManager.setHost("127.0.0.1"); redisManager.setPort(6379); redisManager.setPassword("123456"); return redisManager; } /** * SessionDAO的做用是为Session提供CRUD并进行持久化的一个shiro组件 * MemorySessionDAO 直接在内存中进行会话维护 * EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认状况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。 * @return */ @Bean public SessionDAO sessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); //session在redis中的保存时间,最好大于session会话超时时间 redisSessionDAO.setExpire(12000); return redisSessionDAO; } /** * 配置保存sessionId的cookie * 注意:这里的cookie 不是上面的记住我 cookie 记住我须要一个cookie session管理 也须要本身的cookie * 默认为: JSESSIONID 问题: 与SERVLET容器名冲突,从新定义为sid * @return */ @Bean("sessionIdCookie") public SimpleCookie sessionIdCookie(){ //这个参数是cookie的名称 SimpleCookie simpleCookie = new SimpleCookie("sid"); //setcookie的httponly属性若是设为true的话,会增长对xss防御的安全系数。它有如下特色: //setcookie()的第七个参数 //设为true后,只能经过http访问,javascript没法访问 //防止xss读取cookie simpleCookie.setHttpOnly(true); simpleCookie.setPath("/"); //maxAge=-1表示浏览器关闭时失效此Cookie simpleCookie.setMaxAge(-1); return simpleCookie; } /** * 配置会话管理器,设定会话超时及保存 * @return */ @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(cacheManager()); //全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试 sessionManager.setGlobalSessionTimeout(1800000); //是否开启删除无效的session对象 默认为true sessionManager.setDeleteInvalidSessions(true); //是否开启定时调度器进行检测过时session 默认为true sessionManager.setSessionValidationSchedulerEnabled(true); //设置session失效的扫描时间, 清理用户直接关闭浏览器形成的孤立会话 默认为 1个小时 //设置该属性 就不须要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler //暂时设置为 5秒 用来测试 sessionManager.setSessionValidationInterval(3600000); //取消url 后面的 JSESSIONID sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } /** * 并发登陆控制 * @return */ // @Bean // public KickoutSessionControlFilter kickoutSessionControlFilter(){ // KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); // //用于根据会话ID,获取会话进行踢出操做的; // kickoutSessionControlFilter.setSessionManager(sessionManager()); // //使用cacheManager获取相应的cache来缓存用户登陆的会话;用于保存用户—会话之间的关系的; // kickoutSessionControlFilter.setCacheManager(cacheManager()); // //是否踢出后来登陆的,默认是false;即后者登陆的用户踢出前者登陆的用户; // kickoutSessionControlFilter.setKickoutAfter(false); // //同一个用户最大的会话数,默认1;好比2的意思是同一个用户容许最多同时两我的登陆; // kickoutSessionControlFilter.setMaxSession(1); // //被踢出后重定向到的地址; // kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1"); // return kickoutSessionControlFilter; // } /** * 配置密码比较器 * @return */ // @Bean("credentialsMatcher") // public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher(){ // RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(cacheManager()); // // //若是密码加密,能够打开下面配置 // //加密算法的名称 // //retryLimitHashedCredentialsMatcher.setHashAlgorithmName("MD5"); // //配置加密的次数 // //retryLimitHashedCredentialsMatcher.setHashIterations(1024); // //是否存储为16进制 // //retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); // // return retryLimitHashedCredentialsMatcher; // } }
package com.springboot.test.shiro.config.shiro; import com.springboot.test.shiro.modules.user.dao.PermissionMapper; import com.springboot.test.shiro.modules.user.dao.RoleMapper; import com.springboot.test.shiro.modules.user.dao.entity.Permission; import com.springboot.test.shiro.modules.user.dao.entity.Role; import com.springboot.test.shiro.modules.user.dao.UserMapper; import com.springboot.test.shiro.modules.user.dao.entity.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * @author: wangsaichao * @date: 2018/5/10 * @description: 在Shiro中,最终是经过Realm来获取应用程序中的用户、角色及权限信息的 * 在Realm中会直接从咱们的数据源中获取Shiro须要的验证信息。能够说,Realm是专用于安全框架的DAO. */ public class ShiroRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; /** * 验证用户身份 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //获取用户名密码 第一种方式 //String username = (String) authenticationToken.getPrincipal(); //String password = new String((char[]) authenticationToken.getCredentials()); //获取用户名 密码 第二种方式 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; String username = usernamePasswordToken.getUsername(); String password = new String(usernamePasswordToken.getPassword()); //从数据库查询用户信息 User user = this.userMapper.findByUserName(username); //能够在这里直接对用户名校验,或者调用 CredentialsMatcher 校验 if (user == null) { throw new UnknownAccountException("用户名或密码错误!"); } //这里将 密码对比 注销掉,不然 没法锁定 要将密码对比 交给 密码比较器 //if (!password.equals(user.getPassword())) { // throw new IncorrectCredentialsException("用户名或密码错误!"); //} if ("1".equals(user.getState())) { throw new LockedAccountException("帐号已被锁定,请联系管理员!"); } //调用 CredentialsMatcher 校验 还须要建立一个类 继承CredentialsMatcher 若是在上面校验了,这个就不须要了 //配置自定义权限登陆器 参考博客: SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return info; } /** * 受权用户权限 * 受权的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>标签的时候调用的 * 它会去检测shiro框架中的权限(这里的permissions)是否包含有该标签的name值,若是有,里面的内容显示 * 若是没有,里面的内容不予显示(这就完成了对于权限的认证.) * * shiro的权限受权是经过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo(); * 当访问到页面的时候,连接配置了相应的权限或者shiro标签才会执行此方法不然不会执行 * 因此若是只是简单的身份认证没有权限的控制的话,那么这个方法能够不进行实现,直接返回null便可。 * * 在这个方法中主要是使用类:SimpleAuthorizationInfo 进行角色的添加和权限的添加。 * authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission()); * * 固然也能够添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限 * authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions); * * 就是说若是在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[权限添加]"); * 就说明访问/add这个连接必需要有“权限添加”这个权限才能够访问 * * 若是在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]"); * 就说明访问/add这个连接必需要有 "权限添加" 这个权限和具备 "100002" 这个角色才能够访问 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("查询权限方法调用了!!!"); //获取用户 User user = (User) SecurityUtils.getSubject().getPrincipal(); //获取用户角色 Set<Role> roles =this.roleMapper.findRolesByUserId(user.getUid()); //添加角色 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); for (Role role : roles) { authorizationInfo.addRole(role.getRole()); } //获取用户权限 Set<Permission> permissions = this.permissionMapper.findPermissionsByRoleId(roles); //添加权限 for (Permission permission:permissions) { authorizationInfo.addStringPermission(permission.getPermission()); } return authorizationInfo; } /** * 重写方法,清除当前用户的的 受权缓存 * @param principals */ @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } /** * 重写方法,清除当前用户的 认证缓存 * @param 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(); } }