源码项目地址

尝试登录次数控制实现

实现原理

Realm在验证用户身份的时候,要进行密码匹配。最简单的情况就是明文直接匹配,然后就是加密匹配,这里的匹配工作则就是交给CredentialsMatcher来完成的。我们在这里继承这个接口,自定义一个密码匹配器,缓存入键值对用户名以及匹配次数,若通过密码匹配,则删除该键值对,若不匹配则匹配次数自增。超过给定的次数限制则抛出错误。这里缓存用的是ehcache。

shiro-ehcache配置

maven依赖

<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>1.3.2</version>
</dependency>
复制代码

ehcache配置

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es"><diskStore path="java.io.tmpdir"/><!--name:缓存名称。maxElementsInMemory:缓存最大数目maxElementsOnDisk:硬盘最大缓存个数。eternal:对象是否永久有效,一但设置了,timeout将不起作用。overflowToDisk:是否保存到磁盘,当系统当机时timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。clearOnFlush:内存数量最大时是否清除。memoryStoreEvictionPolicy:Ehcache的三种清空策略;FIFO,first in first out,这个是大家最熟的,先进先出。LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。--><defaultCachemaxElementsInMemory="10000"eternal="false"timeToIdleSeconds="120"timeToLiveSeconds="120"overflowToDisk="false"diskPersistent="false"diskExpiryThreadIntervalSeconds="120"/><!-- 登录记录缓存锁定10分钟 --><cache name="passwordRetryCache"maxEntriesLocalHeap="2000"eternal="false"timeToIdleSeconds="3600"timeToLiveSeconds="0"overflowToDisk="false"statistics="true"></cache></ehcache>
复制代码

#RetryLimitCredentialsMatcher

/** * 验证器,增加了登录次数校验功能 * 此类不对密码加密* @author wgc*/
@Component
public class RetryLimitCredentialsMatcher extends SimpleCredentialsMatcher {  private static final Logger log = LoggerFactory.getLogger(RetryLimitCredentialsMatcher.class);private int maxRetryNum = 5;private EhCacheManager shiroEhcacheManager;public void setMaxRetryNum(int maxRetryNum) {this.maxRetryNum = maxRetryNum;}public RetryLimitCredentialsMatcher(EhCacheManager shiroEhcacheManager) {this.shiroEhcacheManager = shiroEhcacheManager; }@Override  public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {  Cache<String, AtomicInteger> passwordRetryCache = shiroEhcacheManager.getCache("passwordRetryCache");String username = (String) token.getPrincipal();  //retry count + 1  AtomicInteger retryCount = passwordRetryCache.get(username);  if (null == retryCount) {  retryCount = new AtomicInteger(0);passwordRetryCache.put(username, retryCount);  }if (retryCount.incrementAndGet() > maxRetryNum) {log.warn("用户[{}]进行登录验证..失败验证超过{}次", username, maxRetryNum);throw new ExcessiveAttemptsException("username: " + username + " tried to login more than 5 times in period");  }  boolean matches = super.doCredentialsMatch(token, info);  if (matches) {  //clear retry data  passwordRetryCache.remove(username);  }  return matches;  }
}
复制代码

Shiro配置修改

注入CredentialsMatcher

     /*** 缓存管理器* @return cacheManager*/@Beanpublic EhCacheManager ehCacheManager(){EhCacheManager cacheManager = new EhCacheManager();cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");return cacheManager;}/*** 限制登录次数* @return 匹配器*/@Beanpublic CredentialsMatcher retryLimitCredentialsMatcher() {RetryLimitCredentialsMatcher retryLimitCredentialsMatcher = new RetryLimitCredentialsMatcher(ehCacheManager());retryLimitCredentialsMatcher.setMaxRetryNum(5);return retryLimitCredentialsMatcher;}
复制代码

realm添加认证器

myShiroRealm.setCredentialsMatcher(retryLimitCredentialsMatcher());
复制代码

并发在线人数控制实现

KickoutSessionControlFilter


/*** 并发登录人数控制* @author wgc*/
public class KickoutSessionControlFilter extends AccessControlFilter {private static final Logger logger = LoggerFactory.getLogger(KickoutSessionControlFilter.class);/*** 踢出后到的地址*/private String kickoutUrl;/*** 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户*/private boolean kickoutAfter = false;/*** 同一个帐号最大会话数 默认1*/private int maxSession = 1;private String kickoutAttrName = "kickout";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; }/***   设置Cache的key的前缀*/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;} @Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response)throws Exception { Subject subject = getSubject(request, response); if(!subject.isAuthenticated() && !subject.isRemembered()){ //如果没有登录,直接进行之后的流程 return true;} Session session = subject.getSession();UserInfo user = (UserInfo) subject.getPrincipal(); String username = user.getUsername();Serializable sessionId = session.getId();logger.info("进入KickoutControl, sessionId:{}", sessionId);//读取缓存 没有就存入 Deque<Serializable> deque = cache.get(username); if(deque == null) {deque = new LinkedList<Serializable>();  cache.put(username, deque);  }  //如果队列里没有此sessionId,且用户没有被踢出;放入队列if(!deque.contains(sessionId) && session.getAttribute(kickoutAttrName) == null) {//将sessionId存入队列 deque.push(sessionId); } logger.info("deque.size:{}",deque.size());//如果队列里的sessionId数超出最大会话数,开始踢人while(deque.size() > maxSession) { Serializable kickoutSessionId = null; if(kickoutAfter) { //如果踢出后者 kickoutSessionId = deque.removeFirst(); } else { //否则踢出前者 kickoutSessionId = deque.removeLast(); } //踢出后再更新下缓存队列cache.put(username, deque); try { //获取被踢出的sessionId的session对象Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));if(kickoutSession != null) { //设置会话的kickout属性表示踢出了 kickoutSession.setAttribute(kickoutAttrName, true);}} catch (Exception e) {logger.error(e.getMessage());} } //如果被踢出了,直接退出,重定向到踢出后的地址if (session.getAttribute(kickoutAttrName) != null && (Boolean)session.getAttribute(kickoutAttrName) == true) {//会话被踢出了 try { //退出登录subject.logout(); } catch (Exception e) { logger.warn(e.getMessage());e.printStackTrace();}saveRequest(request); //重定向   logger.info("用户登录人数超过限制, 重定向到{}", kickoutUrl);String reason = URLEncoder.encode("账户已超过登录人数限制", "UTF-8");String redirectUrl = kickoutUrl  + (kickoutUrl.contains("?") ? "&" : "?") + "shiroLoginFailure=" + reason;  WebUtils.issueRedirect(request, response, redirectUrl); return false;} return true; }
}
复制代码

ehcache配置

ehcache-shiro.xml加入

    <!-- 用户队列缓存10分钟 --><cache name="shiro-kickout-session"maxEntriesLocalHeap="2000"eternal="false"timeToIdleSeconds="3600"timeToLiveSeconds="0"overflowToDisk="false"statistics="true"></cache>
复制代码

shiro配置

ShiroConfig.java中注入相关对象

     /*** 会话管理器* @return sessionManager*/@Beanpublic DefaultWebSessionManager configWebSessionManager(){DefaultWebSessionManager manager = new DefaultWebSessionManager();// 加入缓存管理器manager.setCacheManager(ehCacheManager());// 删除过期的sessionmanager.setDeleteInvalidSessions(true);// 设置全局session超时时间manager.setGlobalSessionTimeout(1 * 60 *1000);// 是否定时检查sessionmanager.setSessionValidationSchedulerEnabled(true);manager.setSessionValidationScheduler(configSessionValidationScheduler());manager.setSessionIdUrlRewritingEnabled(false);manager.setSessionIdCookieEnabled(true);return manager;}/*** session会话验证调度器* @return session会话验证调度器*/@Beanpublic ExecutorServiceSessionValidationScheduler configSessionValidationScheduler() {ExecutorServiceSessionValidationScheduler sessionValidationScheduler = new ExecutorServiceSessionValidationScheduler();//设置session的失效扫描间隔,单位为毫秒sessionValidationScheduler.setInterval(300*1000);return sessionValidationScheduler;}/*** 限制同一账号登录同时登录人数控制* @return 过滤器*/@Beanpublic KickoutSessionControlFilter kickoutSessionControlFilter() {KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;//这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理//也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性kickoutSessionControlFilter.setCacheManager(ehCacheManager());//用于根据会话ID,获取会话进行踢出操作的;kickoutSessionControlFilter.setSessionManager(configWebSessionManager());//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。kickoutSessionControlFilter.setKickoutAfter(false);//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;kickoutSessionControlFilter.setMaxSession(1);//被踢出后重定向到的地址;kickoutSessionControlFilter.setKickoutUrl("/login");return kickoutSessionControlFilter;}
复制代码

shiro过滤链中加入并发登录人数过滤器

filterChainDefinitionMap.put("/**", "kickout,user");
复制代码

访问任意链接均需要认证通过以及限制并发登录次数

转载于:https://juejin.im/post/5cbe651b6fb9a0322650fbdf

springboot + shiro 尝试登录次数限制与并发登录人数控制相关推荐

  1. 达梦用户登录锁定,登录次数超过限制,登录失败

    查失败次数锁定时间 -- 查失败次数锁定时间 select b.username as "达梦数据库用户名", a.failed_num as "失败次数限制" ...

  2. springboot + shiro之登录人数限制、登录判断重定向、session时间设置

    springboot + shiro之登录人数控制 项目 前篇:spring boot + mybatis + layui + shiro后台权限管理系统:https://blog.51cto.com ...

  3. Java实现用户每天登录次数的限制

    正常的用户每天的登录退出不会太频繁,遇到频繁的登录则很可能是黑客行为.对于黑客行为,我们可以使用登录次数限制来应对.本文将介绍如何限制用户每天的登录次数,包括:"记录当天用户账号的登录次数& ...

  4. SpringBoot+Shiro+ehcache实现登录失败超次数锁定帐号

    文章目录 二.Controller层接收登录请求 三.自定义的Realm 四.密码验证器增加登录次数校验功能 五.ShiroConfig的配置类 六.EhCache 的配置 七.全局异常的配置 ### ...

  5. springboot+shiro+redis+jwt实现多端登录:PC端和移动端同时在线(不同终端可同时在线)

    前言 之前写了篇 springboot+shiro+redis多端登录:单点登录+移动端和PC端同时在线 的文章,但是token用的不是 jwt 而是 sessionID,虽然已经实现了区分pc端和移 ...

  6. Shiro并发登录人数控制遇到的问题和解决

    shiro并发登录人数控制遇到的问题和解决 问题1:KickoutSessionControlFilter不起作用 问题2:KickoutSessionControlFilter中cache为null ...

  7. SpringBoot + Shiro 实现微博登录

    介绍在服务端使用 SpringBoot + Shiro ,用户端使用 jQuery 的环境下如何实现网站对接微博登录 更多精彩 更多技术博客,请移步 IT人才终生实训与职业进阶平台 - 实训在线 写在 ...

  8. 【Shiro】7、Shiro实现控制用户并发登录并踢人下线

    在传统的项目中,同一账户是允许多人同时登录在线的,有的使用场景恰恰是不允许多人同时在线的,那么我们可以通过 Shiro 来控制并发登录,并实现后登录的用户,挤掉前面登录的用户 1.并发登录过滤器 pa ...

  9. 超详细!附源码!SpringBoot+shiro+mybatis+Thymeleaf实现权限登录系统

    最近在做一个期末作品,就是使用ssm+thymeleaf+vue+shiro完成一个具有权限登录,且能实现用户信息增删查改的这么一个项目,下面仅仅是实现权限认证和登录.为什么我选shiro,而不选sp ...

最新文章

  1. [Usaco2006 Nov]Roadblocks第二短路
  2. SpringCloud版本定义说明
  3. 我凭什么拿到了阿里、腾讯、今日头条3家大厂offer?这原因我服了
  4. Windows10下的docker安装与入门 (一)使用docker toolbox安装docker
  5. [Flink] Not a valid protocol version This 1s not an HTTP port
  6. Codeforces 343D Water Tree(DFS序 + 线段树)
  7. Python学习心路历程
  8. 插槽样式_小程序,自定义组件之间的引用,使用插槽扩展组件
  9. Delphi Android menu,Delphi菜单组件TMainMenu使用方法详解
  10. 左边是地狱右边也是地狱_我担任地狱首席执行官的时间
  11. Unity分屏显示效果
  12. 把ubuntu20装在移动固态硬盘
  13. idea 创建一个springboot 项目(hello world)
  14. 【线性DP】跳格子问题 + 光签题(取石子游戏)
  15. vivado 2017.4安装步骤
  16. 计算机网络自顶向下方法 第三章 作业习题答案
  17. 将insert语句转化为select语句,进行校验,验证是否插入成功
  18. 计算机不支持超过2tb的硬盘,适用于超过 2.2TB 容量硬盘的英特尔快速存储技术 (RST) 驱动程序和支持...
  19. 红日靶场vulnstack1 内网渗透学习
  20. cisco模拟器(cisco模拟器怎么显示端口)

热门文章

  1. Java synchronized 详解
  2. Elasticsearch 5.0 —— Head插件部署指南
  3. java高级----Java动态代理的原理
  4. CVPR 2016 有什么值得关注的亮点?
  5. 架构师的能力模型(图)
  6. 浅谈机器学习的职业发展方向
  7. 聚类算法K-Means, K-Medoids, GMM, Spectral clustering,Ncut
  8. VUE搭建手机商城心得
  9. H5学习从0到1-H5的元素属性(3)
  10. 使用WMI对象收集计算机信息