springboot整合shiro-線上人數以及併發登入人數控制(七)
專案中有時候會遇到統計當前線上人數的需求,也有這種情況當A 使用者在邯鄲地區登入 ,然後A使用者在北京地區再登入 ,要踢出邯鄲登入的狀態。如果使用者在北京重新登入,那麼又要踢出邯鄲的使用者,這樣反覆。
這樣保證了一個帳號只能同時一個人使用。那麼下面來講解一下 Shiro 怎麼實現線上人數統計 以及 併發人數控制這個功能。
併發人數控制
參考開濤大神部落格:http://jinnianshilongnian.iteye.com/blog/2039760
使用的技術其實是 shiro的自定義filter,在 springboot整合shiro -快速入門 中 我們已經瞭解到,在shiroConfig的ShiroFilterFactoryBean中使用的過濾規則,如:anon
authc
,user
等本質上是通過呼叫各自對應的filter方式整合的,也就是說,它是遵循過濾器鏈規則的。
如何使用自定義filter實現併發人數的控制?
寫一個KickoutSessionControlFilter類繼承AccessControlFilter類
package com.springboot.test.shiro.config.shiro;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import com.springboot.test.shiro.modules.user.dao.entity.User;
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;
/**
* @author: WangSaiChao
* @date: 2018/5/23
* @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 {
return false;
}
/**
* 表示訪問拒絕時是否自己處理,如果返回true表示自己不處理且繼續攔截器鏈執行,返回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();
//這裡獲取的User是實體 因為我在 自定義ShiroRealm中的doGetAuthenticationInfo方法中
//new SimpleAuthenticationInfo(user, password, getName()); 傳的是 User實體 所以這裡拿到的也是實體,如果傳的是userName 這裡拿到的就是userName
String username = ((User) subject.getPrincipal()).getUsername();
Serializable sessionId = session.getId();
// 初始化使用者的佇列放到快取裡
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;
}
}
注意:
我們首先看一下 isAccessAllowed() 方法,在這個方法中,如果返回 true,則表示“通過”,走到下一個過濾器。如果沒有下一個過濾器的話,表示具有了訪問某個資源的許可權。如果返回 false,則會呼叫 onAccessDenied 方法,去實現相應的當過濾不通過的時候執行的操作,例如檢查使用者是否已經登陸過,如果登陸過,根據自定義規則選擇踢出前一個使用者 還是 後一個使用者。
onAccessDenied方法 返回 true 表示 自己處理完成,然後繼續攔截器鏈執行。
只有當兩者都返回false時,才會終止後面的filter執行。
在shiroConfig中配置該Bean
/**
* 併發登入控制
* @return
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(){
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用於根據會話ID,獲取會話進行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager());
//使用cacheManager獲取相應的cache來快取使用者登入的會話;用於儲存使用者—會話之間的關係的;
kickoutSessionControlFilter.setCacheManager(ehCacheManager());
//是否踢出後來登入的,預設是false;即後者登入的使用者踢出前者登入的使用者;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一個使用者最大的會話數,預設1;比如2的意思是同一個使用者允許最多同時兩個人登入;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出後重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1");
return kickoutSessionControlFilter;
}
修改shiroConfig中shirFilter中配置KickoutSessionControlFilter 並修改過濾規則
/**
* ShiroFilterFactoryBean 處理攔截資原始檔問題。
* 注意:初始化ShiroFilterFactoryBean的時候需要注入:SecurityManager
* Web應用中,Shiro可控制的Web請求必須經過Shiro主過濾器的攔截
* @param securityManager
* @return
*/
@Bean(name = "shirFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
......
//自定義攔截器限制併發人數,參考部落格
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//限制同一帳號同時線上的個數
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
// 配置訪問許可權 必須是LinkedHashMap,因為它必須保證有序
// 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 --> : 這是一個坑,一不小心程式碼就不好使了
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//配置不登入可以訪問的資源,anon 表示資源都可以匿名訪問
//配置記住我或認證通過可以訪問的地址
filterChainDefinitionMap.put("/login", "kickout,anon");
......
//其他資源都需要認證 authc 表示需要認證才能進行訪問 user表示配置記住我或認證通過可以訪問的地址
filterChainDefinitionMap.put("/**", "kickout,user");
return shiroFilterFactoryBean;
}
解釋:
filterChainDefinitionMap.put("/**", "kickout,user");
表示 訪問/**下的資源 首先要通過 kickout 後面的filter,然後再通過user後面對應的filter才可以訪問。
login.html新增踢出登入的資訊提示
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8" />
<title>Insert title here</title>
</head>
<body>
<h1>歡迎登入</h1>
<h1 th:if="${msg != null }" th:text="${msg}" style="color: red"></h1>
<form action="/login" method="post">
使用者名稱:<input type="text" name="username"/><br/>
密碼:<input type="password" name="password"/><br/>
<input type="checkbox" name="rememberMe" />記住我<br/>
<input type="submit" value="提交"/>
</form>
</body>
<script type="text/javascript" th:src="@{/js/jquery.js}"></script>
<script type="text/javascript">
function kickout(){
var href=location.href;
if(href.indexOf("kickout")>0){
alert("您的賬號在另一臺裝置上登入,如非本人操作,請立即修改密碼!");
}
}
window.onload=kickout();
</script>
</html>
測試結果:
統計線上人數
springboot整合shiro-session管理 部落格中,我們有配置過一個監聽類 ,在該類中有統計session建立個數,我們也就用session的個數來統計線上的人數,但是這個統計人數是不準確的,存在這樣一種情況,使用者登入之後,強制退出瀏覽器,再次開啟瀏覽器重新登入,線上人數一直在增加。暫時也沒有想到特別好的方案,有的話留言共同學習。
在LoginController中注入ShiroSessionListener,然後在 index方法中 獲取session 自增數量
model.addAttribute("count",shiroSessionListener.getSessionCount());