登陸人數控制,比如同一個用戶不能在兩個地方登陸。Shiro主要基於自定義的Fliter實現的。本文在上一篇文章Shiro功能應用(四)–Session管理及在線人數統計代碼基礎進行添加登陸人數控制。
代碼實現:
代碼地址:
https://github.com/OooooOz/SpringBoot-Shiro
ShiroConfig的過濾器:
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
... ... ... ...
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("kickout", kickoutSessionControlFilter()); //限制同一帳號同時在線的個數
shiroFilterFactoryBean.setFilters(filtersMap);
//-----------------------------過慮器鏈定義------------------------------//
LinkedHashMap<String, String> perms = new LinkedHashMap<>();
//其他資源都需要認證 authc 表示需要認證才能進行訪問 user表示配置記住我或認證通過可以訪問的地址
perms.put("/userList.do", "user,kickout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(perms); //把權限過濾map設置shiroFilterFactoryBean
return shiroFilterFactoryBean;
}
ShiroConfig的自定義人數控制過濾器:
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(){
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用於根據會話ID,獲取會話進行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager());
//使用cacheManager獲取相應的cache來緩存用戶登錄的會話;用於保存用戶—會話之間的關係的;
kickoutSessionControlFilter.setCacheManager(getEhCacheManager());
//是否踢出後來登錄的,默認是false;即後者登錄的用戶踢出前者登錄的用戶;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一個用戶最大的會話數,默認1;比如2的意思是同一個用戶允許最多同時兩個人登錄;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出後重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/toLogin.do?kickout=1");
return kickoutSessionControlFilter;
}
自定義人數控制過濾器類:
package com.demo.config;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import com.demo.entity.User;
/**
* @description: shiro 自定義filter 實現 併發登錄控制
*/
public class KickoutSessionControlFilter extends AccessControlFilter{
/** 踢出後到的地址 */
private String kickoutUrl;
/** 踢出之前登錄的/之後登錄的用戶 默認踢出之前登錄的用戶 */
private boolean kickoutAfter = false;
/** 同一個帳號最大會話數 默認1 */
private int maxSession = 1;
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;
}
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-activeSessionCache");
}
// 是否允許訪問,返回true表示允許
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
System.out.println("--------------------isAccessAllowed");
return false;
}
//表示訪問拒絕時是否自己處理,如果返回true表示自己不處理且繼續攔截器鏈執行,返回false表示自己已經處理了(比如重定向到另一個頁面)。
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("--------------------onAccessDenied");
Subject subject = getSubject(request, response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
//如果沒有登錄,直接進行之後的流程
return true;
}
Session session = subject.getSession();
String username = ((User) subject.getPrincipal()).getUserName();
Serializable sessionId = session.getId(); //獲取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("kickout") == null) {
deque.push(sessionId);
}
//如果隊列裏的sessionId數超出最大會話數,開始踢人
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) { //如果踢出後者
kickoutSessionId=deque.getFirst();
kickoutSessionId = deque.removeFirst();
} else { //否則踢出前者
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//設置會話的kickout屬性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
e.printStackTrace();
}
}
//如果被踢出了,直接退出,重定向到踢出後的地址
if (session.getAttribute("kickout") != null) {
//會話被踢出了
try {
subject.logout();
} catch (Exception e) {
}
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
}
執行過程:
1).訪問http://localhost:8080/userList.do跳轉登陸的時候,先執行認證的過濾器,即user對應的過濾器,然後再執行自定義的過濾器kickout(perms.put("/userList.do", “user,kickout”);過濾器鏈設置的)
2).在kickout的過濾器中:
如果isAccessAllowed返回false,則繼續訪問onAccessDenied
如果onAccessDenied返回false,則表示在本方法已經處理,就不執行後續的/userList.do控制器處理
如果isAccessAllowed返回true,則不繼續訪問onAccessDenied,直接執行後續控制器處理(當前過濾器結束)
3).在onAccessDenied方法中:
設置了EHCache緩存(kickoutSessionControlFilter.setCacheManager(getEhCacheManager());)第一個用戶登陸,緩存無數據,deque爲null,然後把這個用戶隊列put進緩存,後面把sessionId也push進用戶隊列deque裏,第一個登陸的用戶沒有超過最大會話數,也不是踢出的會話。所以返回true去執行後續控制器邏輯。
第二個用戶登錄的時候,也會把sessionId也push進用戶隊列deque裏,但是超過了最大會話數量maxSession,就會將隊列的sessionId移除( kickoutSessionId = deque.removeLast();返回移除的sessionId),然後被踢出隊列的session設置會話屬性(kickoutSession.setAttribute(“kickout”, true);),然後被踢出的session對應用戶退出登錄。
被踢出用戶再次訪問時,由於session的會話屬性不爲null,所以無法將sessionId加入隊列中,然後返回false,除非再次登陸,踢出其它登陸的session。
參考文章:
springboot整合shiro-在線人數以及併發登錄人數控制(七)