嘗試登入次數控制實現
實現原理
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,最近最少使用的,快取的元素有一個時間戳,當快取容量滿了,而又需要騰出地方來快取新的元素的時候,那麼現有快取元素中時間戳離當前時間最遠的元素將被清出快取。
-->
<defaultCache
maxElementsInMemory="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
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
return cacheManager;
}
/**
* 限制登入次數
* @return 匹配器
*/
@Bean
public 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;
}
@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();
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
*/
@Bean
public DefaultWebSessionManager configWebSessionManager(){
DefaultWebSessionManager manager = new DefaultWebSessionManager();
// 加入快取管理器
manager.setCacheManager(ehCacheManager());
// 刪除過期的session
manager.setDeleteInvalidSessions(true);
// 設定全域性session超時時間
manager.setGlobalSessionTimeout(1 * 60 *1000);
// 是否定時檢查session
manager.setSessionValidationSchedulerEnabled(true);
manager.setSessionValidationScheduler(configSessionValidationScheduler());
manager.setSessionIdUrlRewritingEnabled(false);
manager.setSessionIdCookieEnabled(true);
return manager;
}
/**
* session會話驗證排程器
* @return session會話驗證排程器
*/
@Bean
public ExecutorServiceSessionValidationScheduler configSessionValidationScheduler() {
ExecutorServiceSessionValidationScheduler sessionValidationScheduler = new ExecutorServiceSessionValidationScheduler();
//設定session的失效掃描間隔,單位為毫秒
sessionValidationScheduler.setInterval(300*1000);
return sessionValidationScheduler;
}
/**
* 限制同一賬號登入同時登入人數控制
* @return 過濾器
*/
@Bean
public 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");
複製程式碼
訪問任意連結均需要認證通過以及限制併發登入次數