Springboot+shiro+redis 限制同一賬號 同時在多處登錄


這里的業務場景,就類似與qq 賬號 不能同時在多部手機 登錄一樣,后者會強制前者下線,被強制下線的用戶重新登錄又擠掉 前者,如此反復.....

一. 說下我的思路 (不供參考)

1. 利用Cookie 里面的JESSIONID ,其實也就是sessionid,是可以獲取到的

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

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

4.利用  AccessControlFilter 攔截器

二. 具體步驟

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

2.如果用戶沒有登錄,就直接 進入登錄流程

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

4. 如果 隊列不為空,size 大於0,判斷 隊列中是否包括 這次請求的SessionId 並且 用戶名 是否和保存在Session內的username相同

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異常,可以忽略。");
        }
    }


}

 

 


免責聲明!

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



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