欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

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

发布时间:2025/3/21 编程问答 60 豆豆
生活随笔 收集整理的这篇文章主要介绍了 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

《新程序员》:云原生和全面数字化实践50位技术专家共同创作,文字、视频、音频交互阅读

总结

以上是生活随笔为你收集整理的springboot + shiro 尝试登录次数限制与并发登录人数控制的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。