SpringBoot Shiro並發登錄人數控制


只允許用戶指定n個地點登錄,超過的被移除下線。
參考:https://blog.csdn.net/qq_34021712/article/details/80457041
源碼:https://github.com/Clever-Wang/spring-boot-examples

因為KickoutSessionControlFilter緩存使用的ehCache,暫時只適合單機版本,可以修改為redis支持集群使用。
import com.zihexin.modules.sys.entity.SysUserEntity;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;

/**
  * @Description:shiro 自定義filter 實現 並發登錄控制
  * @Author:SimonHu
  * @Date: 2020/7/15 12:19
  * @return
  */
public class KickoutSessionControlFilter extends AccessControlFilter {
    private static final Logger log = LoggerFactory.getLogger(KickoutSessionControlFilter.class);
    /**
     * 踢出后到的地址
     */
    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()); 傳的是 SysUserEntity實體 所以這里拿到的也是實體,如果傳的是userName 這里拿到的就是userName
        String username = ((SysUserEntity) 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) {
                log.error("--------",e.getMessage());
            }
        }
        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            //會話被踢出了
            try {
                subject.logout();
            } catch (Exception e) {
            }
            WebUtils.issueRedirect(request, response, kickoutUrl);
            return false;
        }
        return true;
    }
}
/**
 * Copyright (c) 2016-2019 人人開源 All rights reserved.
 * <p>
 * https://www.renren.io
 * <p>
 * 版權所有,侵權必究!
 */
package com.zihexin.common.config;

import com.zihexin.common.filter.KickoutSessionControlFilter;
import com.zihexin.modules.sys.shiro.UserRealm;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro的配置文件
 *
 * @author Mark sunlightcs@gmail.com
 */
@Configuration
public class ShiroConfig {
    @Bean
    public EhCacheManager ehCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
        return cacheManager;
    }
    
    /**
     * 單機環境,session交給shiro管理
     */
    @Bean
    @ConditionalOnProperty(prefix = "zihexin", name = "cluster", havingValue = "false")
    public DefaultWebSessionManager sessionManager(@Value("${zihexin.globalSessionTimeout:3600}") long globalSessionTimeout) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setSessionValidationInterval(globalSessionTimeout * 1000);
        sessionManager.setGlobalSessionTimeout(globalSessionTimeout * 1000);
        return sessionManager;
    }
    
    /**
     * 集群環境,session交給spring-session管理
     */
    @Bean
    @ConditionalOnProperty(prefix = "zihexin", name = "cluster", havingValue = "true")
    public ServletContainerSessionManager servletContainerSessionManager() {
        return new ServletContainerSessionManager();
    }
    
    @Bean("securityManager")
    public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        securityManager.setSessionManager(sessionManager);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }
    
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        shiroFilter.setLoginUrl("/login.html");
        shiroFilter.setUnauthorizedUrl("/");
        //自定義攔截器限制並發人數,參考博客
        LinkedHashMap<String, Filter> filtersMap2 = new LinkedHashMap<>();
        filtersMap2.put("kickout", kickoutSessionControlFilter());
        //統計登錄人數
        shiroFilter.setFilters(filtersMap2);
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/statics/**", "anon");
        filterMap.put("/login.html", "kickout,anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/favicon.ico", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/logout", "kickout,anon");
        filterMap.put("/**", "kickout,authc");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }
    
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
    
    /**
     * 並發登錄控制
     *
     * @return
     */
    @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter() {
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        //用於根據會話ID,獲取會話進行踢出操作的;
        kickoutSessionControlFilter.setSessionManager(sessionManager(3600));
        //使用cacheManager獲取相應的cache來緩存用戶登錄的會話;用於保存用戶—會話之間的關系的;
        kickoutSessionControlFilter.setCacheManager(ehCacheManager());
        //是否踢出后來登錄的,默認是false;即后者登錄的用戶踢出前者登錄的用戶;
        kickoutSessionControlFilter.setKickoutAfter(false);
        //同一個用戶最大的會話數,默認1;比如2的意思是同一個用戶允許最多同時兩個人登錄;
        kickoutSessionControlFilter.setMaxSession(1);
        //被踢出后重定向到的地址;
        //這里可以改為登錄頁(需要清除redis相關用戶信息,所以我要跳轉退出頁)
        kickoutSessionControlFilter.setKickoutUrl("/logout?kickout=1");
        return kickoutSessionControlFilter;
    }
}

退出接口,過濾器中其實已經進行了退出操作,我這邊需要進行redis用戶信息和本地cookie的清理,所以重定向到退出接口。

/**
     * 退出
     */
    @RequestMapping(value = "logout", method = RequestMethod.GET)
    public String logout(HttpServletRequest request, HttpServletResponse response,String kickout) {
        sysLoginService.ssoLogOut(request, response);
        ShiroUtils.logout();
        if(StringUtils.isNotEmpty(kickout)){
            return "redirect:login.html?kickout="+kickout;
        }
        return "redirect:login.html";
    }

登錄頁面給出對應提示

<div v-if="error" class="alert alert-danger alert-dismissible">
        <h4 style="margin-bottom: 0px;"><i class="fa fa-exclamation-triangle"></i> {{errorMsg}}</h4>
      </div>

created :function(){
        var url = window.location.href ;
        if(url.indexOf('kickout') >0){
            var cs = url.split('?')[1];
            var param = cs.split('=')[1];
            if(param==1){
                this.errorMsg = '您的賬號在另一處登錄,如非本人操作,請立即修改密碼!'
                this.error = true;
            }
        }

    },

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">

    <!--
        緩存對象存放路徑
        java.io.tmpdir:默認的臨時文件存放路徑。
        user.home:用戶的主目錄。
        user.dir:用戶的當前工作目錄,即當前程序所對應的工作路徑。
        其它通過命令行指定的系統屬性,如“java –DdiskStore.path=D:\\abc ……”。
    -->
    <diskStore path="java.io.tmpdir"/>

    <!--
       name:緩存名稱。
       maxElementsOnDisk:硬盤最大緩存個數。0表示不限制
       maxEntriesLocalHeap:指定允許在內存中存放元素的最大數量,0表示不限制。
       maxBytesLocalDisk:指定當前緩存能夠使用的硬盤的最大字節數,其值可以是數字加單位,單位可以是K、M或者G,不區分大小寫,
                          如:30G。當在CacheManager級別指定了該屬性后,Cache級別也可以用百分比來表示,
                          如:60%,表示最多使用CacheManager級別指定硬盤容量的60%。該屬性也可以在運行期指定。當指定了該屬性后會隱式的使當前Cache的overflowToDisk為true。
       maxEntriesInCache:指定緩存中允許存放元素的最大數量。這個屬性也可以在運行期動態修改。但是這個屬性只對Terracotta分布式緩存有用。
       maxBytesLocalHeap:指定當前緩存能夠使用的堆內存的最大字節數,其值的設置規則跟maxBytesLocalDisk是一樣的。
       maxBytesLocalOffHeap:指定當前Cache允許使用的非堆內存的最大字節數。當指定了該屬性后,會使當前Cache的overflowToOffHeap的值變為true,
                             如果我們需要關閉overflowToOffHeap,那么我們需要顯示的指定overflowToOffHeap的值為false。
       overflowToDisk:boolean類型,默認為false。當內存里面的緩存已經達到預設的上限時是否允許將按驅除策略驅除的元素保存在硬盤上,默認是LRU(最近最少使用)。
                      當指定為false的時候表示緩存信息不會保存到磁盤上,只會保存在內存中。
                      該屬性現在已經廢棄,推薦使用cache元素的子元素persistence來代替,如:<persistence strategy=”localTempSwap”/>。
       diskSpoolBufferSizeMB:當往磁盤上寫入緩存信息時緩沖區的大小,單位是MB,默認是30。
       overflowToOffHeap:boolean類型,默認為false。表示是否允許Cache使用非堆內存進行存儲,非堆內存是不受Java GC影響的。該屬性只對企業版Ehcache有用。
       copyOnRead:當指定該屬性為true時,我們在從Cache中讀數據時取到的是Cache中對應元素的一個copy副本,而不是對應的一個引用。默認為false。
       copyOnWrite:當指定該屬性為true時,我們在往Cache中寫入數據時用的是原對象的一個copy副本,而不是對應的一個引用。默認為false。
       timeToIdleSeconds:單位是秒,表示一個元素所允許閑置的最大時間,也就是說一個元素在不被請求的情況下允許在緩存中待的最大時間。默認是0,表示不限制。
       timeToLiveSeconds:單位是秒,表示無論一個元素閑置與否,其允許在Cache中存在的最大時間。默認是0,表示不限制。
       eternal:boolean類型,表示是否永恆,默認為false。如果設為true,將忽略timeToIdleSeconds和timeToLiveSeconds,Cache內的元素永遠都不會過期,也就不會因為元素的過期而被清除了。
       diskExpiryThreadIntervalSeconds :單位是秒,表示多久檢查元素是否過期的線程多久運行一次,默認是120秒。
       clearOnFlush:boolean類型。表示在調用Cache的flush方法時是否要清空MemoryStore。默認為true。
       diskPersistent:是否緩存虛擬機重啟期數據 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
       maxElementsInMemory:緩存最大數目
       memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU(最近最少使用)。你可以設置為FIFO(先進先出)或是LFU(較少使用)。
            memoryStoreEvictionPolicy:
               Ehcache的三種清空策略;
               FIFO,first in first out,這個是大家最熟的,先進先出。
               LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。
               LRU,Least Recently Used,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又需要騰出地方來緩存新的元素的時候,那么現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。
    -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="0"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />

    <!-- 授權緩存 -->
    <cache name="authorizationCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <!-- 認證緩存 -->
    <cache name="authenticationCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

    <!-- session緩存 -->
    <cache name="shiro-activeSessionCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

</ehcache>
 <!-- 配置ehcache緩存 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM