Springboot+shiro+redis 限制同一帳號 同時在多處登陸

這裏的業務場景,就相似與qq 帳號 不能同時在多部手機 登陸同樣,後者會強制前者下線,被強制下線的用戶從新登陸又擠掉 前者,如此反覆.....

一. 說下個人思路 (不供參考)java

1. 利用Cookie 裏面的JESSIONID ,其實也就是sessionid,是能夠獲取到的web

2.利用 Deque 雙向隊列,最大 maxSize 設置爲1redis

3.利用redis 緩存 ,將 Deque 存入緩存spring

4.利用  AccessControlFilter 攔截器apache

二. 具體步驟json

1. 每次請求獲取 到sessionId ,和用戶名 緩存

2.若是用戶沒有登陸,就直接 進入登陸流程springboot

3.根據 用戶來 獲取 Deque 若是沒有,新建立,將sessionid 放入隊列 用戶名放入session內session

4. 若是 隊列不爲空,size 大於0,判斷 隊列中是否包括 此次請求的SessionId 而且 用戶名 是否和保存在Session內的username相同app

5.若是 知足上述條件,隊列中沒包括該SessionId 且 用戶名相同,就把此次 請求的SessionId 放入隊列,此時 Deque 的size>1

6.判斷 若是 隊列的Size>1, 就 踢出舊的SessionId. (隊列 添加數據是 從頭部加入,首先 加入的會排在後面),

7.更新緩存

package com.example.springboot.shiro.core.shiro.filter;

import com.example.springboot.shiro.user.entity.Uuser;
import net.sf.json.JSONObject;
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 javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutUrl; //踢出後到的地址
    private boolean kickoutAfter = false; //踢出以前登陸的/以後登陸的用戶 默認踢出以前登陸的用戶
    private int maxSession = 1; //同一個賬號最大會話數 默認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;
    }

    //設置Cache的key的前綴
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro_redis_cache:");
    }

    /**
     * 表示是否容許訪問;mappedValue就是[urls]配置中攔截器參數部分,若是容許訪問返回true,不然false;
     * (感受這裏應該是對白名單(不須要登陸的接口)放行的)
     * 若是isAccessAllowed返回true則onAccessDenied方法不會繼續執行
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     * @throws Exception
     */

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        System.err.println(">>>>>>>>>>>>>>>>Session 隊列>>>>>>>>>>>>>>>>>>");


        Subject subject = getSubject(request, response);
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            //若是沒有登陸,直接進行登陸的流程
            return true;
        }
        Session session = subject.getSession();
        Uuser user = (Uuser) subject.getPrincipal();
        String username = user.getEmail();
        Serializable sessionId = session.getId();

        //讀取緩存   沒有就存入
        Deque<Serializable> deque = cache.get(username);

        //若是此用戶沒有session隊列,也就是尚未登陸過,緩存中沒有
        //就new一個空隊列,否則deque對象爲空,會報空指針
        if (deque == null) {
            deque = new LinkedList<Serializable>();
            deque.push(sessionId);
            session.setAttribute("username", username);
            cache.put(username, deque);
        }
        String name = String.valueOf(session.getAttribute("username"));
           session.setAttribute("kickout",true);

        //若是隊列裏沒有此sessionId,且用戶沒有被踢出;放入隊列
        if (!deque.contains(sessionId) && name.equals(username)) {
            //將sessionId存入隊列
            deque.push(sessionId);

            //將用戶的sessionId隊列緩存
            cache.put(username, deque);
        }

        //若是隊列裏的sessionId數超出最大會話數,開始踢人
        Session kickoutSession = null;
        while (deque.size() > maxSession) {
            Serializable kickoutSessionId = null;

            kickoutSessionId = deque.removeLast();
            //踢出後再更新下緩存隊列
            cache.put(username, deque);


            try {
                //獲取被踢出的sessionId的session對象
                kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null) {
                    //設置會話的kickout屬性表示踢出了
                    session.setAttribute("kickout", false);
                }
            } catch (Exception e) {//ignore exception
            }
        }

        //若是被踢出了,直接退出,重定向到踢出後的地址

        if (!((Boolean) session.getAttribute("kickout"))) {


            Map<String, String> resultMap = new HashMap<String, String>();
            //判斷是否是Ajax請求
            if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
                resultMap.put("user_status", "300");
                resultMap.put("message", "您已經在其餘地方登陸,請從新登陸!");
                //輸出json串
                out(response, resultMap);
                System.out.println("您已經在其餘地方登陸,請從新登陸!");
                return Boolean.FALSE;
            }

        }
        return Boolean.TRUE;

    }


    /**
     * 表示當訪問拒絕時是否已經處理了;若是返回true表示須要繼續處理;
     * 若是返回false表示該攔截器實例已經處理了,將直接返回便可。
     * onAccessDenied是否執行取決於isAccessAllowed的值,
     * 若是返回true則onAccessDenied不會執行;若是返回false,執行onAccessDenied
     * 若是onAccessDenied也返回false,則直接返回,
     * 不會進入請求的方法(只有isAccessAllowed和onAccessDenied的狀況下)
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //退出
        Subject subject = getSubject(request, response);
        subject.logout();
        //保存訪問路徑
        saveRequest(request);
        //重定向
        WebUtils.issueRedirect(request, response, kickoutUrl);

        return Boolean.FALSE;
    }


    private void out(ServletResponse hresponse, Map<String, String> resultMap)
            throws IOException {
        try {
            hresponse.setCharacterEncoding("UTF-8");
            PrintWriter out = hresponse.getWriter();
            out.println(JSONObject.fromObject(resultMap).toString());
            out.flush();
            out.close();
        } catch (Exception e) {
            System.err.println("KickoutSessionFilter.class 輸出JSON異常,能夠忽略。");
        }
    }


}
相關文章
相關標籤/搜索