亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Shiro實現(xiàn)session限制登錄數(shù)量踢人下線功能

 更新時間:2023年11月11日 10:57:11   作者:bbq烤雞  
這篇文章主要介紹了Shiro實現(xiàn)session限制登錄數(shù)量踢人下線,本文記錄的是shiro采用session作為登錄方案時,對用戶進行限制數(shù)量登錄,以及剔除下線,需要的朋友可以參考下

前言

近年無狀態(tài)登錄興起,但sessionId方式仍是主流方案,借用類似redis集群等方案存儲session信息使得它也足以跟上微服務的浪潮。相對來說session方式更具有服務端控制感,而無狀態(tài)登錄要想實現(xiàn)服務端控制就得存儲些東西,這么一來無狀態(tài)就得打上一個問號。本文記錄的是shiro采用session作為登錄方案時,對用戶進行限制數(shù)量登錄,以及剔除下線。

實現(xiàn)

■ 架構準備

首先搭建好基于redis存儲session的shiro鑒權框架底子,網(wǎng)上很容易找到各種實現(xiàn)代碼。

ShiroConfig

找到spring中的ShiroConfig,應有類似如下代碼

// 自定義授權緩存管理器
實現(xiàn) CacheManager 的授權緩存管理器,改用redis存儲授權信息。

@Bean
public JedisCacheManager shiroCacheManager() {
	JedisCacheManager shiroCacheManager = new JedisCacheManager();
	return shiroCacheManager;
}

// 自定義Session存儲容器
繼承 AbstractSessionDAO 實現(xiàn) SessionDAO ,對session的curd的具體實現(xiàn)方法自定義編寫,采用redis存儲與操作。也是本文的主要修改類。

@Bean
public JedisSessionDAO sessionDAO(IdGen idGen) {
	JedisSessionDAO sessionDAO = new JedisSessionDAO();
	sessionDAO.setSessionIdGenerator(idGen);
	sessionDAO.setSessionKeyPrefix(redis_keyPrefix + "_session:");
	return sessionDAO;
}

// 自定義會話管理配置
繼承 DefaultWebSessionManager 的自定義WEB會話管理類。

@Bean
public SessionManager sessionManager(JedisSessionDAO sessionDAO, SimpleCookie sessionIdCookie) {
    SessionManager sessionManager = new SessionManager();
    sessionManager.setSessionDAO(sessionDAO);
    // 會話超時時間,單位:毫秒
    sessionManager.setGlobalSessionTimeout(session_sessionTimeout);
    sessionManager.setSessionValidationSchedulerEnabled(true);
    sessionManager.setSessionIdCookie(sessionIdCookie);
    sessionManager.setSessionIdCookieEnabled(true);
    return sessionManager;
}

// 自定義Shiro安全管理配置

@Bean
public DefaultWebSecurityManager securityManager(SystemAuthorizingRealm systemAuthorizingRealm, SessionManager sessionManager
        , JedisCacheManager shiroCacheManager) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(systemAuthorizingRealm);
    securityManager.setSessionManager(sessionManager);
    securityManager.setCacheManager(shiroCacheManager);
    return securityManager;
}

這些配置一層套一層,其它的省略了。。。主要修改的就是JedisSessionDAO

■ redis內的存儲分布

如圖,
上面一堆是session的存儲,存儲的字符串類型,key為前綴+sessionId,value為session內容;
下面一堆則是輔助session限制登陸的存儲,key為前綴+userId,value則是map集合,map的key為sessionId,value可以存儲一些我們需要的內容,此處我存的是session的最后活動時間。
這么設計即可少許的redis操作就達到我們的目的——限制登陸和踢人下線。

注:key的存儲命名使用:分隔是因為低版本的RDM默認使用:符號分隔歸檔,方便我們的可視化查詢,高版本以及其它工具是可以自定義分隔符的。

■ 代碼修改

修改 JedisSessionDAO

新增以下方法,并對實現(xiàn)的接口 SessionDAO 添加抽象方法。
這個方法在登錄時調用,用于判斷一個賬號登錄session的數(shù)量并剔除超出規(guī)則的賬號。

@Override
public Collection<Session> limitSessions(Object principal){
	// principal在這個方法指的就是userID
    if (principal != null){
        principal = principal.toString();
    }
    // 等會兒取出來的用戶存活的session需要放入這個list進行時間排序,以剔除過舊的session。
    ArrayList<Session> sessions = new ArrayList();
    Jedis jedis = null;
    try {
        jedis = JedisUtils.getResource();
        // 查詢該userId的session map集合。
        Map<String, String> map = jedis.hgetAll(sessionUserKeyPrefix + principal);
        for (Map.Entry<String, String> e : map.entrySet()){
        	// 遍歷集合,剔除不規(guī)范的內容,一般來說是不會出現(xiàn)的
            if (StringUtils.isNotBlank(e.getKey()) && StringUtils.isNotBlank(e.getValue())){
            	// 最后活動時間
                String expire = e.getValue();
                // 因為session的具體存儲在redis的字符串中,可以自動過期,
                // 而這里session信息存儲在map集合的其中一條鍵值對中無法設置自動過期,
                // 所以需要借助SimpleSession類對session是否存活進行校驗。
                // 每當該賬號有認證操作時就會更新一遍map。
                if (StringUtils.isNotBlank(expire)){
                    SimpleSession session = new SimpleSession();
                    session.setId(e.getKey());
                    session.setAttribute("principalId", principal);
                    session.setTimeout(TokenUtils.cacheSeconds * 1000);
                    session.setLastAccessTime(new Date(Long.valueOf(expire)));
                    try{
                        // 驗證SESSION
                        session.validate();
                        sessions.add(session);
                    }
                    // SESSION驗證失敗
                    catch (Exception e2) {
                        jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());
                    }
                }
                // 存儲的SESSION不符合規(guī)則
                else{
                    jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());
                }
            }
            // 存儲的SESSION無Value
            else if (StringUtils.isNotBlank(e.getKey())){
                jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());
            }
        }
        // 剔除過期的session后得到的 sessions.size() 才是當前賬號所存活的session
        logger.info("該賬戶 session 數(shù)量: {} ", sessions.size());
        // 我定義的規(guī)則:如果存活的session大于某個值,就對sessions進行時間排序,并且剔除最后操作較早的session
        if(sessions.size() > SESSIONLIMTI) {
            sessions.sort(new Comparator<Session>() {
                @Override
                public int compare(Session o1, Session o2) {
                    return (int)(o1.getLastAccessTime().getTime() - o2.getLastAccessTime().getTime());
                }
            });
            for (int i = 0; i < sessions.size() - SESSIONLIMTI; i++) {
                Session session = sessions.get(i);
                jedis.hdel(sessionUserKeyPrefix + principal, session.getId().toString());
                jedis.del(JedisUtils.getBytesKey(sessionKeyPrefix + session.getId()));
            }
        }
    } catch (Exception e) {
        logger.error("limitSessions", e);
    } finally {
        JedisUtils.returnResource(jedis);
    }
    return sessions;
}

修改 SystemAuthorizingRealm

如下代碼,doGetAuthenticationInfo 是shiro認證的回調函數(shù),重寫內容一般有登錄校驗、登錄日志之類,在這里就可以追加限制登錄數(shù)量和剔除session的操作,也就是調用前面編寫的方法。

/**
 * 認證回調函數(shù), 登錄時調用
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
    UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
    // 校驗登錄驗證碼
//業(yè)務校驗。。。。。。省略
    // 校驗用戶名密碼以及賬號是否凍結
    User user = getSystemService().。。。。。。
    if (user != null) {
        if (Global.NO.equals(user.getLoginFlag())) {
            throw new AuthenticationException("msg:該帳號已禁止登錄.");
        } else if (Global.YES.equals(user.getBlacklist())) {
            throw new AuthenticationException("msg:該帳號已被加入黑名單.");
        }
        byte[] salt = Encodes.decodeHex(。。。);
        Principal principal = new Principal(user, 。。。);
        // 無痕登錄 不打日志
        if(token.isTraceless()) {
            principal.setTraceless(true);
        } else {
            // 更新登錄IP和時間
            getSystemService().updateUserLoginInfo(user);
            // 記錄登錄日志
            LogUtils.saveLog(Servlets.getRequest(), "系統(tǒng)登錄", user);
            // 踢人
            int limitSessionSize = getSystemService().getSessionDao().limitSessions(user.getId()).size();
        }
        return new SimpleAuthenticationInfo(principal, 。。。);
    } else {
        return null;
    }
}

新增 ApiLogoutFilter

重寫 preHandle 方法,如果退出登錄,就從map中移除該session,我本來是打算寫在 JedisSessionDAOdelete方法中,但是執(zhí)行到這個方法的時候已經(jīng)清除了用戶信息,所以無法獲得userId,當然可以采用再設置一個sessionId所對應的redis存儲輔助,有些冗余,可能有更好的切入點寫入,我目前是寫在這里。

public class ApiLogoutFilter extends LogoutFilter {
    private static final Logger log = LoggerFactory.getLogger(ApiLogoutFilter.class);
    private String sessionUserKeyPrefix = "jes_map:";
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = this.getSubject(request, response);
        if (this.isPostOnlyLogout() && !WebUtils.toHttp(request).getMethod().toUpperCase(Locale.ENGLISH).equals("POST")) {
            return this.onLogoutRequestNotAPost(request, response);
        } else {
            String redirectUrl = this.getRedirectUrl(request, response, subject);
            try {
                SystemAuthorizingRealm.Principal principal = (SystemAuthorizingRealm.Principal)subject.getPrincipal();
                String sessionId = subject.getSession().getId().toString();
                subject.logout();
                JedisUtils.mapRemove(sessionUserKeyPrefix + principal, sessionId);
            } catch (SessionException var6) {
                log.debug("Encountered session exception during logout.  This can generally safely be ignored.", var6);
            }
            this.issueRedirect(request, response, redirectUrl);
            return false;
        }
    }
}

再次修改 JedisSessionDAO

這個方法里就可以獲取userId了,如下代碼就可以設置與更新這個登錄的map集合,以及更新session的生命周期。

@Override
public void update(Session session) throws UnknownSessionException {
	if (session == null || session.getId() == null) {
        return;
    }
   	/** 現(xiàn)在項目基本前后端分離 這一段基本沒用
	HttpServletRequest request = Servlets.getRequest();
	if (request != null){
		String uri = request.getServletPath();
		// 如果是靜態(tài)文件,則不更新SESSION
		if (Servlets.isStaticFile(uri)){
			return;
		}
		// 如果是視圖文件,則不更新SESSION
		if (StringUtils.startsWith(uri, Global.getConfig("web.view.prefix"))
				&& StringUtils.endsWith(uri, Global.getConfig("web.view.suffix"))){
			return;
		}
		// 手動控制不更新SESSION
		if (Global.NO.equals(request.getParameter("updateSession"))){
			return;
		}
	}
**/
	Jedis jedis = null;
	try {
		jedis = JedisUtils.getResource();
		// 獲取登錄者編號
		PrincipalCollection pc = (PrincipalCollection)session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
		String principalId = pc != null ? pc.getPrimaryPrincipal().toString() : StringUtils.EMPTY;
        if (StringUtils.isNotBlank(principalId)) {
            jedis.hset(sessionUserKeyPrefix + principalId, session.getId().toString(),   "" + session.getLastAccessTime().getTime());
            jedis.expire(sessionUserKeyPrefix + principalId, TokenUtils.cacheSeconds);
        }
		jedis.set(JedisUtils.getBytesKey(sessionKeyPrefix + session.getId()), JedisUtils.toBytes(session));
		// 設置超期時間
		int timeoutSeconds = (int)(session.getTimeout() / 1000);
		jedis.expire((sessionKeyPrefix + session.getId()), timeoutSeconds);
		logger.debug("update {} {}", session.getId(), request != null ? request.getRequestURI() : "");
	} catch (Exception e) {
		logger.error("update {} {}", session.getId(), request != null ? request.getRequestURI() : "", e);
	} finally {
		JedisUtils.returnResource(jedis);
	}
}

最后

在此,我只是規(guī)定了固定數(shù)量規(guī)則,這個限制登錄數(shù)量當然可以是存儲于關系型數(shù)據(jù)庫里和賬號綁定的,甚至可以是花里胡哨的規(guī)則,例如——手機登錄限制只能登錄1個,瀏覽器登錄限制10個。還可以通過ws推送,主動告知被剔除的那個客戶端——您的賬號在福建省XX市XX登錄,您被踢下線,如有異常,申請凍結賬號。甚至可以列出登錄設備列表,讓客戶可以選擇性的剔除哪個設備。只要在map里存儲的時間戳修改為這些豐富的數(shù)據(jù),就能實現(xiàn)這些很有趣的功能。

到此這篇關于Shiro實現(xiàn)session限制登錄數(shù)量踢人下線的文章就介紹到這了,更多相關Shiro實現(xiàn)session限制登錄數(shù)量內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Java中的==使用方法詳解

    Java中的==使用方法詳解

    這篇文章主要給大家介紹了關于Java中的==使用方法的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-09-09
  • Spring計劃任務用法實例詳解

    Spring計劃任務用法實例詳解

    這篇文章主要介紹了Spring計劃任務用法,結合實例形式詳細分析了spring計劃任務相關原理、配置、使用方法及操作注意事項,需要的朋友可以參考下
    2019-11-11
  • java實現(xiàn)的AES加密算法完整實例

    java實現(xiàn)的AES加密算法完整實例

    這篇文章主要介紹了java實現(xiàn)的AES加密算法,結合完整實例形式分析了AES加密類的實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2016-07-07
  • Spring @Cacheable redis異常不影響正常業(yè)務方案

    Spring @Cacheable redis異常不影響正常業(yè)務方案

    這篇文章主要介紹了Spring @Cacheable redis異常不影響正常業(yè)務方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-02-02
  • Java實現(xiàn)單例模式之餓漢式、懶漢式、枚舉式

    Java實現(xiàn)單例模式之餓漢式、懶漢式、枚舉式

    本篇文章主要介紹了Java實現(xiàn)單例的3種普遍的模式,餓漢式、懶漢式、枚舉式。具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2016-10-10
  • springboot訪問template下的html頁面的實現(xiàn)配置

    springboot訪問template下的html頁面的實現(xiàn)配置

    這篇文章主要介紹了springboot訪問template下的html頁面的實現(xiàn)配置,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-12-12
  • Springboot項目異常處理及返回結果統(tǒng)一

    Springboot項目異常處理及返回結果統(tǒng)一

    這篇文章主要介紹了Springboot項目異常處理及返回結果統(tǒng)一,文章圍繞主題展開詳細的內容介紹,具有一定的參考價值,需要的朋友可以參考一下
    2022-08-08
  • SpringBoot?使用AOP?+?Redis?防止表單重復提交的方法

    SpringBoot?使用AOP?+?Redis?防止表單重復提交的方法

    Spring?Boot是一個用于構建Web應用程序的框架,通過AOP可以實現(xiàn)防止表單重復提交,本文介紹了在Spring?Boot應用程序中使用AOP和Redis來防止表單重復提交的方法,需要的朋友可以參考下
    2023-04-04
  • Toolbar制作菜單條過程詳解

    Toolbar制作菜單條過程詳解

    Toolbar制作菜單條過程詳解...
    2006-12-12
  • Java Map所有的值轉為String類型

    Java Map所有的值轉為String類型

    本文主要介紹了Java Map所有的值轉為String類型,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-05-05

最新評論