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

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

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

前言

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

實(shí)現(xiàn)

■ 架構(gòu)準(zhǔn)備

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

ShiroConfig

找到spring中的ShiroConfig,應(yīng)有類(lèi)似如下代碼

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

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

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

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

// 自定義會(huì)話管理配置
繼承 DefaultWebSessionManager 的自定義WEB會(huì)話管理類(lèi)。

@Bean
public SessionManager sessionManager(JedisSessionDAO sessionDAO, SimpleCookie sessionIdCookie) {
    SessionManager sessionManager = new SessionManager();
    sessionManager.setSessionDAO(sessionDAO);
    // 會(huì)話超時(shí)時(shí)間,單位:毫秒
    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內(nèi)的存儲(chǔ)分布

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

注:key的存儲(chǔ)命名使用:分隔是因?yàn)榈桶姹镜腞DM默認(rèn)使用:符號(hào)分隔歸檔,方便我們的可視化查詢(xún),高版本以及其它工具是可以自定義分隔符的。

■ 代碼修改

修改 JedisSessionDAO

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

@Override
public Collection<Session> limitSessions(Object principal){
	// principal在這個(gè)方法指的就是userID
    if (principal != null){
        principal = principal.toString();
    }
    // 等會(huì)兒取出來(lái)的用戶(hù)存活的session需要放入這個(gè)list進(jìn)行時(shí)間排序,以剔除過(guò)舊的session。
    ArrayList<Session> sessions = new ArrayList();
    Jedis jedis = null;
    try {
        jedis = JedisUtils.getResource();
        // 查詢(xún)?cè)搖serId的session map集合。
        Map<String, String> map = jedis.hgetAll(sessionUserKeyPrefix + principal);
        for (Map.Entry<String, String> e : map.entrySet()){
        	// 遍歷集合,剔除不規(guī)范的內(nèi)容,一般來(lái)說(shuō)是不會(huì)出現(xiàn)的
            if (StringUtils.isNotBlank(e.getKey()) && StringUtils.isNotBlank(e.getValue())){
            	// 最后活動(dòng)時(shí)間
                String expire = e.getValue();
                // 因?yàn)閟ession的具體存儲(chǔ)在redis的字符串中,可以自動(dòng)過(guò)期,
                // 而這里session信息存儲(chǔ)在map集合的其中一條鍵值對(duì)中無(wú)法設(shè)置自動(dòng)過(guò)期,
                // 所以需要借助SimpleSession類(lèi)對(duì)session是否存活進(jìn)行校驗(yàn)。
                // 每當(dāng)該賬號(hào)有認(rèn)證操作時(shí)就會(huì)更新一遍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{
                        // 驗(yàn)證SESSION
                        session.validate();
                        sessions.add(session);
                    }
                    // SESSION驗(yàn)證失敗
                    catch (Exception e2) {
                        jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());
                    }
                }
                // 存儲(chǔ)的SESSION不符合規(guī)則
                else{
                    jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());
                }
            }
            // 存儲(chǔ)的SESSION無(wú)Value
            else if (StringUtils.isNotBlank(e.getKey())){
                jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());
            }
        }
        // 剔除過(guò)期的session后得到的 sessions.size() 才是當(dāng)前賬號(hào)所存活的session
        logger.info("該賬戶(hù) session 數(shù)量: {} ", sessions.size());
        // 我定義的規(guī)則:如果存活的session大于某個(gè)值,就對(duì)sessions進(jìn)行時(shí)間排序,并且剔除最后操作較早的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認(rèn)證的回調(diào)函數(shù),重寫(xiě)內(nèi)容一般有登錄校驗(yàn)、登錄日志之類(lèi),在這里就可以追加限制登錄數(shù)量和剔除session的操作,也就是調(diào)用前面編寫(xiě)的方法。

/**
 * 認(rèn)證回調(diào)函數(shù), 登錄時(shí)調(diào)用
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
    UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
    // 校驗(yàn)登錄驗(yàn)證碼
//業(yè)務(wù)校驗(yàn)。。。。。。省略
    // 校驗(yàn)用戶(hù)名密碼以及賬號(hào)是否凍結(jié)
    User user = getSystemService().。。。。。。
    if (user != null) {
        if (Global.NO.equals(user.getLoginFlag())) {
            throw new AuthenticationException("msg:該帳號(hào)已禁止登錄.");
        } else if (Global.YES.equals(user.getBlacklist())) {
            throw new AuthenticationException("msg:該帳號(hào)已被加入黑名單.");
        }
        byte[] salt = Encodes.decodeHex(。。。);
        Principal principal = new Principal(user, 。。。);
        // 無(wú)痕登錄 不打日志
        if(token.isTraceless()) {
            principal.setTraceless(true);
        } else {
            // 更新登錄IP和時(shí)間
            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

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

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

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

@Override
public void update(Session session) throws UnknownSessionException {
	if (session == null || session.getId() == null) {
        return;
    }
   	/** 現(xiàn)在項(xiàng)目基本前后端分離 這一段基本沒(méi)用
	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;
		}
		// 手動(dòng)控制不更新SESSION
		if (Global.NO.equals(request.getParameter("updateSession"))){
			return;
		}
	}
**/
	Jedis jedis = null;
	try {
		jedis = JedisUtils.getResource();
		// 獲取登錄者編號(hào)
		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));
		// 設(shè)置超期時(shí)間
		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ī)則,這個(gè)限制登錄數(shù)量當(dāng)然可以是存儲(chǔ)于關(guān)系型數(shù)據(jù)庫(kù)里和賬號(hào)綁定的,甚至可以是花里胡哨的規(guī)則,例如——手機(jī)登錄限制只能登錄1個(gè),瀏覽器登錄限制10個(gè)。還可以通過(guò)ws推送,主動(dòng)告知被剔除的那個(gè)客戶(hù)端——您的賬號(hào)在福建省XX市XX登錄,您被踢下線,如有異常,申請(qǐng)凍結(jié)賬號(hào)。甚至可以列出登錄設(shè)備列表,讓客戶(hù)可以選擇性的剔除哪個(gè)設(shè)備。只要在map里存儲(chǔ)的時(shí)間戳修改為這些豐富的數(shù)據(jù),就能實(shí)現(xiàn)這些很有趣的功能。

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

相關(guān)文章

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

    Java中的==使用方法詳解

    這篇文章主要給大家介紹了關(guān)于Java中的==使用方法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-09-09
  • Spring計(jì)劃任務(wù)用法實(shí)例詳解

    Spring計(jì)劃任務(wù)用法實(shí)例詳解

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

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

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

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

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

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

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

    springboot訪問(wèn)template下的html頁(yè)面的實(shí)現(xiàn)配置

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

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

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

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

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

    Toolbar制作菜單條過(guò)程詳解

    Toolbar制作菜單條過(guò)程詳解...
    2006-12-12
  • Java Map所有的值轉(zhuǎn)為String類(lèi)型

    Java Map所有的值轉(zhuǎn)為String類(lèi)型

    本文主要介紹了Java Map所有的值轉(zhuǎn)為String類(lèi)型,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-05-05

最新評(píng)論