maven添加依赖
<!-- shiro权限控制框架. --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency>
<dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>2.8.24</version> </dependency>
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
添加redis 配置类
@Configuration public class RedisConfig extends CachingConfigurerSupport { private final Logger logger = LoggerFactory.getLogger(RedisConfig.class); @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.pool.max-wait}") private long maxWaitMillis; @Bean public JedisPool redisPoolFactory() { logger.info("JedisPool注入成功!!"); logger.info("redis地址:" + host + ":" + port); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMaxWaitMillis(maxWaitMillis); JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout); return jedisPool; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory){ StringRedisTemplate redisTemplate = new StringRedisTemplate(redisConnectionFactory); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); /** * 通用的序列化和反序列化设置 * ObjectMapper类是Jackson库的主要类。它提供一些功能将转换成Java对象匹配JSON结构,反之亦然。 */ ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setValueSerializer(stringRedisSerializer); /*redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(stringRedisSerializer);*/ redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
添加shiro 配置类
@Configuration public class ShiroConfiguration { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Bean(name = "lifecycleBeanPostProcessor") public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 标签使用thymeleaf * @return */ @Bean(name = "shiroDialect") public ShiroDialect shiroDialect() { return new ShiroDialect(); } /** * ShiroFilterFactoryBean 处理拦截资源文件问题。 * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在 * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过 3、部分过滤器可指定参数,如perms,roles * */ @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();//获取filters filters.put("authc", formAuthenticationFilter());//将自定义 的FormAuthenticationFilter注入shiroFilter中 //限制同一帐号同时在线的个数。 filters.put("kickout", kickoutSessionControlFilter()); //拦截器. Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/kickout","anon"); filterChainDefinitionMap.put("/static/**","anon"); filterChainDefinitionMap.put("/login","authc"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/index","authc,kickout"); filterChainDefinitionMap.put("/a/**","authc,kickout"); //filterChainDefinitionMap.put("/**","user");// 只是为了掩饰 记住我功能 //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了; //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> //自定义加载权限资源关系 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean(name = "securityManager") public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //设置realm. securityManager.setRealm(myShiroRealm()); // 自定义缓存实现 使用redis securityManager.setCacheManager(cacheManager()); // 自定义session管理 使用redis securityManager.setSessionManager(sessionManager()); //注入记住我管理器; //securityManager.setRememberMeManager(rememberMeManager()); return securityManager; } /** * cookie对象; * @return */ /*@Bean public SimpleCookie getSimpleCookie(){ //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe SimpleCookie simpleCookie = new SimpleCookie(); simpleCookie.setName("rememberMe"); //<!-- 记住我cookie生效时间30天 ,单位秒;--> simpleCookie.setMaxAge(180000); return simpleCookie; }*/ /** * cookie管理对象;记住我功能 * @return */ /*@Bean public CookieRememberMeManager rememberMeManager(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位) cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; }*/ @Bean public Realm myShiroRealm(){ Realm realm = new Realm(); /*realm.setCachingEnabled(true); realm.setAuthenticationCachingEnabled(true); realm.setAuthenticationCacheName("authenticationCache"); realm.setAuthorizationCachingEnabled(true); realm.setAuthorizationCacheName("authorizationCache");*/ realm.setCredentialsMatcher(getRetryLimitHashedCredentialsMatcher()); realm.setCacheManager(cacheManager()); return realm; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setSessionIdGenerator(getJavaUuidSessionIdGenerator()); return redisSessionDAO; } /** * 会话ID生成器 * @return */ @Bean public JavaUuidSessionIdGenerator getJavaUuidSessionIdGenerator(){ return new JavaUuidSessionIdGenerator(); } /*@Bean(name = "ehcacheManager") public EhCacheManager cacheManager() { EhCacheManager ehCacheManager = new EhCacheManager(); ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); return ehCacheManager; }*/ /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * @return */ @Bean(name = "redisManager") public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setExpire(1800);// 配置缓存过期时间 redisManager.setTimeout(timeout); // redisManager.setPassword(password); return redisManager; } /** * shiro session的管理 */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); //sessionManager.setSessionDAO(getEnterpriseCacheSessionDAO()); sessionManager.setSessionDAO(redisSessionDAO()); /* 默认是开启的,在会话过期后会调用 SessionDAO 的 delete 方法删除会话*/ sessionManager.setDeleteInvalidSessions(true); /*是否启用/禁用 Session Id Cookie,默认是启用的*/ //sessionManager.setSessionIdCookieEnabled(true); /*是 sessionManager 创建会话 Cookie 的模板:*/ //sessionManager.setSessionIdCookie(getSimpleCookie()); /*另外可以设置会话的全局过期时间(毫秒为单位),默认 30 分钟:*/ sessionManager.setGlobalSessionTimeout(1800000);//30分钟 sessionManager.setCacheManager(cacheManager()); return sessionManager; } /** * 凭证匹配器 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * 所以我们需要修改下doGetAuthenticationInfo中的代码; * ) * @return */ @Bean(name = "hashedCredentialsMatcher") public RetryLimitHashedCredentialsMatcher getRetryLimitHashedCredentialsMatcher(){ RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(cacheManager()); retryLimitHashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; retryLimitHashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return retryLimitHashedCredentialsMatcher; } @Bean public MyFormAuthenticationFilter formAuthenticationFilter(){ MyFormAuthenticationFilter formAuthenticationFilter = new MyFormAuthenticationFilter(); formAuthenticationFilter.setUsernameParam("username"); formAuthenticationFilter.setPasswordParam("password"); //formAuthenticationFilter.setRememberMeParam("rememberMe"); return formAuthenticationFilter; } /** * 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持; Controller才能使用@RequiresPermissions * * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( @Qualifier("securityManager") SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 限制同一账号登录同时登录人数控制 * @return */ @Bean public KickoutSessionControlFilter kickoutSessionControlFilter(){ KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); //使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的; //这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理 //也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性 kickoutSessionControlFilter.setCacheManager(cacheManager()); //用于根据会话ID,获取会话进行踢出操作的; kickoutSessionControlFilter.setSessionManager(sessionManager()); //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。 kickoutSessionControlFilter.setKickoutAfter(false); //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录; kickoutSessionControlFilter.setMaxSession(1); //被踢出后重定向到的地址; kickoutSessionControlFilter.setKickoutUrl("/kickout"); return kickoutSessionControlFilter; } }
添加同一用户登陆次数控制
package com.dcy.shiro.filter; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionKey; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; import java.util.Deque; import java.util.LinkedList; /** * Created by Administrator on 2018/5/16. */ public class KickoutSessionControlFilter extends AccessControlFilter{ private String kickoutUrl; //踢出后到的地址 private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户 private int maxSession = 1; //同一个帐号最大会话数 默认1 private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache; public void setKickoutUrl(String kickoutUrl) { this.kickoutUrl = kickoutUrl; } public void setKickoutAfter(boolean kickoutAfter) { this.kickoutAfter = kickoutAfter; } public void setMaxSession(int maxSession) { this.maxSession = maxSession; } public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } public void setCacheManager(CacheManager cacheManager) { this.cache = cacheManager.getCache("shiro-kickout-session"); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); if(!subject.isAuthenticated() && !subject.isRemembered()) { //如果没有登录,直接进行之后的流程 return true; } Session session = subject.getSession(); String username = (String) subject.getPrincipal(); Serializable sessionId = session.getId(); System.out.println("sessionId-=-"+sessionId); //TODO 同步控制 Deque<Serializable> deque = cache.get(username); if( null == deque|| deque.size() == 0) { deque = new LinkedList<Serializable>(); //cache.put(username, deque); } //如果队列里没有此sessionId,且用户没有被踢出;放入队列 if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) { deque.push(sessionId); //将用户的sessionId队列缓存 cache.put(username, deque); } //如果队列里的sessionId数超出最大会话数,开始踢人 while(deque.size() > maxSession) { Serializable kickoutSessionId = null; if(kickoutAfter) { //如果踢出后者 kickoutSessionId = deque.removeFirst(); //踢出后再更新下缓存队列 cache.put(username, deque); } else { //否则踢出前者 kickoutSessionId = deque.removeLast(); //踢出后再更新下缓存队列 cache.put(username, deque); } try { Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId)); if(kickoutSession != null) { //设置会话的kickout属性表示踢出了 kickoutSession.setAttribute("kickout", true); } } catch (Exception e) {//ignore exception } } //如果被踢出了,直接退出,重定向到踢出后的地址 if (session.getAttribute("kickout") != null) { //会话被踢出了 try { subject.logout(); } catch (Exception e) { //ignore } saveRequest(request); WebUtils.issueRedirect(request, response, kickoutUrl); return false; } return true; } }
添加用户登陆错误次数控制
/** * 用户登录验证 */ public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(RedisCacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { // TODO Auto-generated method stub String username = (String)token.getPrincipal(); //retry count + 1 AtomicInteger retryCount = passwordRetryCache.get(username); if(retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry count passwordRetryCache.remove(username); }else { //如果不正确 从新put passwordRetryCache.put(username, retryCount); } return matches; } }
添加realm
public class Realm extends AuthorizingRealm { private final Logger logger = LoggerFactory.getLogger(Realm.class); @Autowired private ISysUserService iSysUserService; @Autowired private ISysAreaService iSysAreaService; //用户登录次数计数 redisKey 前缀 private String SHIRO_LOGIN_COUNT = "shiro_login_count_"; //用户登录是否被锁定 一小时 redisKey 前缀 private String SHIRO_IS_LOCK = "shiro_is_lock_"; @Autowired private RedisTemplate redisTemplate; /** * 授权 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo authorizationInfo = null; try { //获取登录名 String username = (String) principalCollection.getPrimaryPrincipal(); authorizationInfo = new SimpleAuthorizationInfo(); //获取登陆人有哪些角色 authorizationInfo.setRoles(iSysUserService.findRoleNameByUserName(username)); //获取登陆人有哪些权限 authorizationInfo.setStringPermissions(iSysUserService.findMenuNameByUserName(username)); } catch (Exception e) { e.printStackTrace(); } return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //logger.info(" 认证 "); //token中储存着输入的用户名和密码 //UsernamePasswordToken upToken = (UsernamePasswordToken)token; String userName = (String) authenticationToken.getPrincipal(); /*logger.info(" 登录名"+upToken.getUsername()); logger.info(" 密码名"+String.valueOf(upToken.getPassword()));*/ //访问一次,计数一次 /*ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); opsForValue.increment(SHIRO_LOGIN_COUNT + userName, 1); Integer count = Integer.parseInt(redisTemplate.opsForValue().get(SHIRO_LOGIN_COUNT + userName).toString()); //计数大于5时,设置用户被锁定一小时 if (count >= 5) { opsForValue.set(SHIRO_IS_LOCK + userName, "LOCK"); redisTemplate.expire(SHIRO_IS_LOCK + userName, 1, TimeUnit.HOURS); } if ("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK + userName))) { throw new DisabledAccountException("由于密码输入错误次数大于5次,帐号已经禁止登录!"); }*/ //根据登录名查询对象 SysUser sysUser = iSysUserService.findByUsername(userName); if (sysUser == null) { throw new UnknownAccountException();//没找到帐号 } if (Boolean.TRUE.equals(sysUser.getFlag())) { throw new LockedAccountException(); //帐号锁定 } //清空登录计数 //opsForValue.set(SHIRO_LOGIN_COUNT+userName, "0"); //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( sysUser.getUsername(), //用户名 sysUser.getPassword(), //密码 ByteSource.Util.bytes(sysUser.getCredentialsSalt()),//salt=username+salt //ByteSourceUtils.bytes(sysUser.getCredentialsSalt()), getName() //realm name ); //存到session this.setSession(Constant.LOGONUSER, sysUser, sysUser.getId()); return authenticationInfo; } private Session getSession() { try { Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(false); if (session == null) { session = subject.getSession(); } if (session != null) { return session; } } catch (InvalidSessionException e) { } return null; } /** * 保存登录名 */ private void setSession(Object key, Object value, Long userId) { try { Session session = getSession(); //System.out.println("Session默认超时时间为[" + session.getTimeout() + "]毫秒"); if (null != session) { String sessionId = (String) session.getId(); //System.out.println(sessionId); //userService.updateLastLogin(sessionId, session.getLastAccessTime(), session.getHost(), userId); session.setAttribute(key, value); } } catch (InvalidSessionException e) { e.printStackTrace(); } } /** * 清空之前 授权 缓存的AuthorizationInfo */ @Override protected void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } /** * 清空之前 认证 缓存的AuthenticationInfo */ @Override protected void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { clearCachedAuthorizationInfo(principals); clearCachedAuthenticationInfo(principals); } /** * 授权 */ public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } /** * 认证 */ public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } /** * 清除全部缓存 */ public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }
添加登陆拦截器
/** * Created by Administrator on 2017/12/25. */ public class MyFormAuthenticationFilter extends FormAuthenticationFilter { @Override protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception { //System.out.println("登陆成功-=-=-==--==="); WebUtils.issueRedirect(request, response, "/index"); /*SysUser sysUser = Global.getSubject(); switch (sysUser.getSysArea().getType()){ case 1: WebUtils.issueRedirect(request, response, "/count/index"); break; default: WebUtils.issueRedirect(request, response, "/index"); break; }*/ } }