SpringBoot+Shiro學習之自定義攔截器管理在線用戶(踢出用戶)

sf2270 7年前發布 | 61K 次閱讀 Spring Boot Apache Shiro

  1. 我們經常會有用到,當A 用戶在北京登錄 ,然后A用戶在天津再登錄 ,要踢出北京登錄的狀態。如果用戶在北京重新登錄,那么又要踢出天津的用戶,這樣反復。又或是需要限制同一用戶的同時在線數量,超出限制后,踢出最先登錄的或是踢出最后登錄的。

  2. 第一個場景踢出用戶是由用戶觸發的,有時候需要手動將某個在線用戶踢出,也就是對當前在線用戶的列表進行管理。

實現思路

spring security就直接提供了相應的功能;Shiro的話沒有提供默認實現,不過可以很容易的在Shiro中加入這個功能。那就是使用shiro強大的自定義訪問控制攔截器:AccessControlFilter,集成這個接口后要實現下面這三個方法。

abstractbooleanisAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)throwsException;

booleanonAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue)throwsException;

abstractbooleanonAccessDenied(ServletRequest request, ServletResponse response)throwsException; </code></pre>

isAccessAllowed:表示是否允許訪問;mappedValue就是[urls]配置中攔截器參數部分,如果允許訪問返回true,否則false;

onAccessDenied:表示當訪問拒絕時是否已經處理了;如果返回true表示需要繼續處理;如果返回false表示該攔截器實例已經處理了,將直接返回即可。

onPreHandle:會自動調用這兩個方法決定是否繼續處理;

另外AccessControlFilter還提供了如下方法用于處理如登錄成功后/重定向到上一個請求:

voidsetLoginUrl(String loginUrl)//身份驗證時使用,默認/login.jsp
String getLoginUrl()  
Subject getSubject(ServletRequest request, ServletResponse response) //獲取Subject實例
boolean isLoginRequest(ServletRequest request, ServletResponse response)//當前請求是否是登錄請求
void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //將當前請求保存起來并重定向到登錄頁面
void saveRequest(ServletRequest request) //將請求保存起來,如登錄成功后再重定向回該請求
void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登錄頁面

比如基于表單的身份驗證就需要使用這些功能。

到此基本的攔截器就完事了,如果我們想進行訪問的控制就可以繼承AccessControlFilter;如果我們要添加一些通用數據我們可以直接繼承PathMatchingFilter。

下面就是我實現的訪問控制攔截器:KickoutSessionControlFilter:

/**

  • @author 作者 z77z
  • @date 創建時間:2017年3月5日 下午1:16:38
  • 思路:
  • 1.讀取當前登錄用戶名,獲取在緩存中的sessionId隊列
  • 2.判斷隊列的長度,大于最大登錄限制的時候,按踢出規則
  • 將之前的sessionId中的session域中存入kickout:true,并更新隊列緩存
  • 3.判斷當前登錄的session域中的kickout如果為true,
  • 想將其做退出登錄處理,然后再重定向到踢出登錄提示頁面 */ public classKickoutSessionControlFilterextendsAccessControlFilter{

    private String kickoutUrl; //踢出后到的地址 private boolean kickoutAfter = false; //踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶 private int maxSession = 1; //同一個帳號最大會話數 默認1

    private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache;

    publicvoidsetKickoutUrl(String kickoutUrl){

     this.kickoutUrl = kickoutUrl;
    

    }

    publicvoidsetKickoutAfter(booleankickoutAfter){

     this.kickoutAfter = kickoutAfter;
    

    }

    publicvoidsetMaxSession(intmaxSession){

     this.maxSession = maxSession;
    

    }

    publicvoidsetSessionManager(SessionManager sessionManager){

     this.sessionManager = sessionManager;
    

    } //設置Cache的key的前綴 publicvoidsetCacheManager(CacheManager cacheManager){

     this.cache = cacheManager.getCache("shiro_redis_cache");
    

    }

    @Override protectedbooleanisAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)throwsException{

     return false;
    

    }

    @Override protectedbooleanonAccessDenied(ServletRequest request, ServletResponse response)throwsException{

     Subject subject = getSubject(request, response);
     if(!subject.isAuthenticated() && !subject.isRemembered()) {
         //如果沒有登錄,直接進行之后的流程
         return true;
     }
    
     Session session = subject.getSession();
     SysUser user = (SysUser) subject.getPrincipal();
     String username = user.getNickname();
     Serializable sessionId = session.getId();
    
     //讀取緩存 沒有就存入
     Deque<Serializable> deque = cache.get(username);
    
     //如果隊列里沒有此sessionId,且用戶沒有被踢出;放入隊列
     if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
         //將sessionId存入隊列
         deque.push(sessionId);
         //將用戶的sessionId隊列緩存
         cache.put(username, deque);
     }
    
     //如果隊列里的sessionId數超出最大會話數,開始踢人
     while(deque.size() > maxSession) {
         Serializable kickoutSessionId = null;
         if(kickoutAfter) { //如果踢出后者
             kickoutSessionId = deque.removeFirst();
         } else { //否則踢出前者
             kickoutSessionId = deque.removeLast();
         }
         //踢出后再更新下緩存隊列
         cache.put(username, deque);
        try {
            //獲取被踢出的sessionId的session對象
            Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
            if(kickoutSession != null) {
                //設置會話的kickout屬性表示踢出了
                kickoutSession.setAttribute("kickout", true);
            }
        } catch (Exception e) {//ignore exception
        }
    }

    //如果被踢出了,直接退出,重定向到踢出后的地址
    if ((Boolean)session.getAttribute("kickout")!=null&&(Boolean)session.getAttribute("kickout") == true) {
        //會話被踢出了
        try {
            //退出登錄
            subject.logout();
        } catch (Exception e) { //ignore
        }
        saveRequest(request);
        //重定向
        WebUtils.issueRedirect(request, response, kickoutUrl);
        return false;
    }
    return true;
}

} </code></pre>

將這個自定義的攔截器配置在ShiroConfig.java文件中:

/**

  • 限制同一賬號登錄同時登錄人數控制
  • @return */ publicKickoutSessionControlFilterkickoutSessionControlFilter(){ KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); //使用cacheManager獲取相應的cache來緩存用戶登錄的會話;用于保存用戶—會話之間的關系的; //這里我們還是用之前shiro使用的redisManager()實現的cacheManager()緩存管理 //也可以重新另寫一個,重新配置緩存時間之類的自定義緩存屬性 kickoutSessionControlFilter.setCacheManager(cacheManager()); //用于根據會話ID,獲取會話進行踢出操作的; kickoutSessionControlFilter.setSessionManager(sessionManager()); //是否踢出后來登錄的,默認是false;即后者登錄的用戶踢出前者登錄的用戶;踢出順序。 kickoutSessionControlFilter.setKickoutAfter(false); //同一個用戶最大的會話數,默認1;比如2的意思是同一個用戶允許最多同時兩個人登錄; kickoutSessionControlFilter.setMaxSession(1); //被踢出后重定向到的地址; kickoutSessionControlFilter.setKickoutUrl("/kickout"); return kickoutSessionControlFilter; } </code></pre>

    將這個kickoutSessionControlFilter()注入到shiroFilterFactoryBean中:

    //自定義攔截器
    Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
    //限制同一帳號同時在線的個數。
    filtersMap.put("kickout", kickoutSessionControlFilter());
    shiroFilterFactoryBean.setFilters(filtersMap);
    

    由于我們鏈接權限的控制是動態存在數據庫中的,這個可以去看我之前動態權限控制的博文,所以我們還要在數據庫中修改鏈接的權限,將kickout這個自定義的權限配置在對應的鏈接上。如下圖:

    權限表

    還要編寫對應的被踢出的跳轉頁面:

    <%@pagelanguage="java"contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://"
            + request.getServerName() + ":" + request.getServerPort()
            + path;
    %>
    <!DOCTYPE html>
    <html>
    <head>
    <metahttp-equiv="Content-Type"content="text/html; charset=UTF-8">
    <scripttype="text/javascript"
    src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
    <title>被踢出</title>
    </head>
    <body>
    被踢出 或則在另一地方登錄,或已經達到此賬號登錄上限被擠掉。
    <inputtype="button"id="login"value="重新登錄"/>
    </body>
    <scripttype="text/javascript">
    $("#login").click(function(){
    window.open("<%=basePath%>/login"); 
    });
    </script>
    </html>
    

    到此,第一個場景就實現了,寫到這里實際第二個場景的實現思路已經就很明顯了,可以通過sessionDAO獲取到全部的shiro會話List,然后顯示在前端頁面,踢出對應用戶就可以使用在對應sessionId的session域中設置key為kickout的值為true,上面的KickoutSessionControlFilter就會判斷session域中的kickout值,做響應的處理。這里我就先不上代碼了,大家可以自己試一試。之后再把代碼同步到我的碼云上,供大家學習交流。

    處理了這個需求后,我發現一個問題,這里有一個前提,我們知道Ajax不能做頁面redirect和forward跳轉,所以Ajax請求假如沒登錄,那么這個請求給用戶的感覺就是沒有任何反應,而用戶又不知道用戶已經退出了。這個就要對ajax請求做相應的優化,我已經有解決思路了,大家也可以思考下,我也會在下一博提供代碼。

    還有我接下來會對之前的前端頁面進行完善,比如下面是我更新的登錄頁面:

    登錄頁面

     

     

    來自:http://z77z.oschina.io/2017/03/05/SpringBoot Shiro學習之自定義攔截器管理在線用戶(踢出用戶)/

     

 本文由用戶 sf2270 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!