1. 程式人生 > >springboot整合shiro-線上人數以及併發登入人數控制(七)

springboot整合shiro-線上人數以及併發登入人數控制(七)

專案中有時候會遇到統計當前線上人數的需求,也有這種情況當A 使用者在邯鄲地區登入 ,然後A使用者在北京地區再登入 ,要踢出邯鄲登入的狀態。如果使用者在北京重新登入,那麼又要踢出邯鄲的使用者,這樣反覆。
這樣保證了一個帳號只能同時一個人使用。那麼下面來講解一下 Shiro 怎麼實現線上人數統計 以及 併發人數控制這個功能。

併發人數控制

參考開濤大神部落格:http://jinnianshilongnian.iteye.com/blog/2039760
使用的技術其實是 shiro的自定義filter,在 springboot整合shiro -快速入門 中 我們已經瞭解到,在shiroConfig的ShiroFilterFactoryBean中使用的過濾規則,如:anon

authcuser等本質上是通過呼叫各自對應的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());