springboot + shiro 尝试登录次数限制与并发登录人数控制
源码项目地址
尝试登录次数控制实现
实现原理
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 尝试登录次数限制与并发登录人数控制相关推荐
- 达梦用户登录锁定,登录次数超过限制,登录失败
查失败次数锁定时间 -- 查失败次数锁定时间 select b.username as "达梦数据库用户名", a.failed_num as "失败次数限制" ...
- springboot + shiro之登录人数限制、登录判断重定向、session时间设置
springboot + shiro之登录人数控制 项目 前篇:spring boot + mybatis + layui + shiro后台权限管理系统:https://blog.51cto.com ...
- Java实现用户每天登录次数的限制
正常的用户每天的登录退出不会太频繁,遇到频繁的登录则很可能是黑客行为.对于黑客行为,我们可以使用登录次数限制来应对.本文将介绍如何限制用户每天的登录次数,包括:"记录当天用户账号的登录次数& ...
- SpringBoot+Shiro+ehcache实现登录失败超次数锁定帐号
文章目录 二.Controller层接收登录请求 三.自定义的Realm 四.密码验证器增加登录次数校验功能 五.ShiroConfig的配置类 六.EhCache 的配置 七.全局异常的配置 ### ...
- springboot+shiro+redis+jwt实现多端登录:PC端和移动端同时在线(不同终端可同时在线)
前言 之前写了篇 springboot+shiro+redis多端登录:单点登录+移动端和PC端同时在线 的文章,但是token用的不是 jwt 而是 sessionID,虽然已经实现了区分pc端和移 ...
- Shiro并发登录人数控制遇到的问题和解决
shiro并发登录人数控制遇到的问题和解决 问题1:KickoutSessionControlFilter不起作用 问题2:KickoutSessionControlFilter中cache为null ...
- SpringBoot + Shiro 实现微博登录
介绍在服务端使用 SpringBoot + Shiro ,用户端使用 jQuery 的环境下如何实现网站对接微博登录 更多精彩 更多技术博客,请移步 IT人才终生实训与职业进阶平台 - 实训在线 写在 ...
- 【Shiro】7、Shiro实现控制用户并发登录并踢人下线
在传统的项目中,同一账户是允许多人同时登录在线的,有的使用场景恰恰是不允许多人同时在线的,那么我们可以通过 Shiro 来控制并发登录,并实现后登录的用户,挤掉前面登录的用户 1.并发登录过滤器 pa ...
- 超详细!附源码!SpringBoot+shiro+mybatis+Thymeleaf实现权限登录系统
最近在做一个期末作品,就是使用ssm+thymeleaf+vue+shiro完成一个具有权限登录,且能实现用户信息增删查改的这么一个项目,下面仅仅是实现权限认证和登录.为什么我选shiro,而不选sp ...
最新文章
- [Usaco2006 Nov]Roadblocks第二短路
- SpringCloud版本定义说明
- 我凭什么拿到了阿里、腾讯、今日头条3家大厂offer?这原因我服了
- Windows10下的docker安装与入门 (一)使用docker toolbox安装docker
- [Flink] Not a valid protocol version This 1s not an HTTP port
- Codeforces 343D Water Tree(DFS序 + 线段树)
- Python学习心路历程
- 插槽样式_小程序,自定义组件之间的引用,使用插槽扩展组件
- Delphi Android menu,Delphi菜单组件TMainMenu使用方法详解
- 左边是地狱右边也是地狱_我担任地狱首席执行官的时间
- Unity分屏显示效果
- 把ubuntu20装在移动固态硬盘
- idea 创建一个springboot 项目(hello world)
- 【线性DP】跳格子问题 + 光签题(取石子游戏)
- vivado 2017.4安装步骤
- 计算机网络自顶向下方法 第三章 作业习题答案
- 将insert语句转化为select语句,进行校验,验证是否插入成功
- 计算机不支持超过2tb的硬盘,适用于超过 2.2TB 容量硬盘的英特尔快速存储技术 (RST) 驱动程序和支持...
- 红日靶场vulnstack1 内网渗透学习
- cisco模拟器(cisco模拟器怎么显示端口)