springboot + shiro 尝试登录次数限制与并发登录人数控制
生活随笔
收集整理的這篇文章主要介紹了
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 尝试登录次数限制与并发登录人数控制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于web开发字符集问题解决方法
- 下一篇: 7-1 最大子列和问题