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

應(yīng)用場(chǎng)景

  1. 我們經(jīng)常會(huì)有用到,當(dāng)A 用戶在北京登錄 ,然后A用戶在天津再登錄 ,要踢出北京登錄的狀態(tài)。如果用戶在北京重新登錄,那么又要踢出天津的用戶,這樣反復(fù)。又或是需要限制同一用戶的同時(shí)在線數(shù)量,超出限制后,踢出最先登錄的或是踢出最后登錄的。

  2. 第一個(gè)場(chǎng)景踢出用戶是由用戶觸發(fā)的,有時(shí)候需要手動(dòng)將某個(gè)在線用戶踢出,也就是對(duì)當(dāng)前在線用戶的列表進(jìn)行管理。

·························································································································································
個(gè)人博客:http://z77z.oschina.io/

此項(xiàng)目下載地址:https://git.oschina.net/z77z/springboot_mybatisplus
························································································································································

實(shí)現(xiàn)思路

spring security就直接提供了相應(yīng)的功能;Shiro的話沒(méi)有提供默認(rèn)實(shí)現(xiàn),不過(guò)可以很容易的在Shiro中加入這個(gè)功能。那就是使用shiro強(qiáng)大的自定義訪問(wèn)控制攔截器:AccessControlFilter,集成這個(gè)接口后要實(shí)現(xiàn)下面這三個(gè)方法。

abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  

boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception; 
 
abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;   

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

onAccessDenied:表示當(dāng)訪問(wèn)拒絕時(shí)是否已經(jīng)處理了;如果返回true表示需要繼續(xù)處理;如果返回false表示該攔截器實(shí)例已經(jīng)處理了,將直接返回即可。

onPreHandle:會(huì)自動(dòng)調(diào)用這兩個(gè)方法決定是否繼續(xù)處理;

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

void setLoginUrl(String loginUrl) //身份驗(yàn)證時(shí)使用,默認(rèn)/login.jsp  
String getLoginUrl()  
Subject getSubject(ServletRequest request, ServletResponse response) //獲取Subject實(shí)例  
boolean isLoginRequest(ServletRequest request, ServletResponse response)//當(dāng)前請(qǐng)求是否是登錄請(qǐng)求  
void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //將當(dāng)前請(qǐng)求保存起來(lái)并重定向到登錄頁(yè)面  
void saveRequest(ServletRequest request) //將請(qǐng)求保存起來(lái),如登錄成功后再重定向回該請(qǐng)求  
void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登錄頁(yè)面   

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

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

下面就是我實(shí)現(xiàn)的訪問(wèn)控制攔截器:KickoutSessionControlFilter:

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

    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = false; //踢出之前登錄的/之后登錄的用戶 默認(rèn)踢出之前登錄的用戶
    private int maxSession = 1; //同一個(gè)帳號(hào)最大會(huì)話數(shù) 默認(rèn)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;
    }
    //設(shè)置Cache的key的前綴
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if(!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果沒(méi)有登錄,直接進(jìn)行之后的流程
            return true;
        }

        Session session = subject.getSession();
        SysUser user = (SysUser) subject.getPrincipal();
        String username = user.getNickname();
        Serializable sessionId = session.getId();

        //讀取緩存   沒(méi)有就存入
        Deque<Serializable> deque = cache.get(username);
        
        //如果隊(duì)列里沒(méi)有此sessionId,且用戶沒(méi)有被踢出;放入隊(duì)列
        if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            //將sessionId存入隊(duì)列
            deque.push(sessionId);
            //將用戶的sessionId隊(duì)列緩存
            cache.put(username, deque);
        }

        //如果隊(duì)列里的sessionId數(shù)超出最大會(huì)話數(shù),開(kāi)始踢人
        while(deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if(kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
            } else { //否則踢出前者
                kickoutSessionId = deque.removeLast();
            }
            //踢出后再更新下緩存隊(duì)列
            cache.put(username, deque);
            
            
            try {
                //獲取被踢出的sessionId的session對(duì)象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if(kickoutSession != null) {
                    //設(shè)置會(huì)話的kickout屬性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }

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

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

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

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

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

由于我們鏈接權(quán)限的控制是動(dòng)態(tài)存在數(shù)據(jù)庫(kù)中的,這個(gè)可以去看我之前動(dòng)態(tài)權(quán)限控制的博文,所以我們還要在數(shù)據(jù)庫(kù)中修改鏈接的權(quán)限,將kickout這個(gè)自定義的權(quán)限配置在對(duì)應(yīng)的鏈接上。如下圖:

權(quán)限表

還要編寫(xiě)對(duì)應(yīng)的被踢出的跳轉(zhuǎn)頁(yè)面:

<%@ page language="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>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript"
    src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
<title>被踢出</title>
</head>
<body>
被踢出 或則在另一地方登錄,或已經(jīng)達(dá)到此賬號(hào)登錄上限被擠掉。
<input type="button" id="login" value="重新登錄" />
</body>
<script type="text/javascript">
$("#login").click(function(){
    window.open("<%=basePath%>/login"); 
});
</script>
</html>

到此,第一個(gè)場(chǎng)景就實(shí)現(xiàn)了,寫(xiě)到這里實(shí)際第二個(gè)場(chǎng)景的實(shí)現(xiàn)思路已經(jīng)就很明顯了,可以通過(guò)sessionDAO獲取到全部的shiro會(huì)話List,然后顯示在前端頁(yè)面,踢出對(duì)應(yīng)用戶就可以使用在對(duì)應(yīng)sessionId的session域中設(shè)置key為kickout的值為true,上面的KickoutSessionControlFilter就會(huì)判斷session域中的kickout值,做響應(yīng)的處理。這里我就先不上代碼了,大家可以自己試一試。之后再把代碼同步到我的碼云上,供大家學(xué)習(xí)交流。

處理了這個(gè)需求后,我發(fā)現(xiàn)一個(gè)問(wèn)題,這里有一個(gè)前提,我們知道Ajax不能做頁(yè)面redirect和forward跳轉(zhuǎn),所以Ajax請(qǐng)求假如沒(méi)登錄,那么這個(gè)請(qǐng)求給用戶的感覺(jué)就是沒(méi)有任何反應(yīng),而用戶又不知道用戶已經(jīng)退出了。這個(gè)就要對(duì)ajax請(qǐng)求做相應(yīng)的優(yōu)化,我已經(jīng)有解決思路了,大家也可以思考下,我也會(huì)在下一博提供代碼。

還有我接下來(lái)會(huì)對(duì)之前的前端頁(yè)面進(jìn)行完善,比如下面是我更新的登錄頁(yè)面:

登錄頁(yè)面

已經(jīng)更新到我的碼云上面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容