Shiro功能應用(五)--Session管理的登陸人數控制


     登陸人數控制,比如同一個用戶不能在兩個地方登陸。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-在線人數以及併發登錄人數控制(七)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章