Shiro实现session限制登录数量踢人下线

article/2025/9/17 23:41:29

Shiro实现session限制登录数量踢人下线

    • 前言
    • 实现
      • ■ 架构准备
        • ShiroConfig
      • ■ redis内的存储分布
      • ■ 代码修改
        • 修改 JedisSessionDAO
        • 修改 SystemAuthorizingRealm
        • 新增 ApiLogoutFilter
        • 再次修改 JedisSessionDAO
    • 最后

前言

近年无状态登录兴起,但sessionId方式仍是主流方案,借用类似redis集群等方案存储session信息使得它也足以跟上微服务的浪潮。相对来说session方式更具有服务端控制感,而无状态登录要想实现服务端控制就得存储些东西,这么一来无状态就得打上一个问号。本文记录的是shiro采用session作为登录方案时,对用户进行限制数量登录,以及剔除下线。

实现

■ 架构准备

首先搭建好基于redis存储session的shiro鉴权框架底子,网上很容易找到各种实现代码。

ShiroConfig

找到spring中的ShiroConfig,应有类似如下代码

// 自定义授权缓存管理器
实现 CacheManager 的授权缓存管理器,改用redis存储授权信息。

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

// 自定义Session存储容器
继承 AbstractSessionDAO 实现 SessionDAO ,对session的curd的具体实现方法自定义编写,采用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

新增以下方法,并对实现的接口 SessionDAO 添加抽象方法。
这个方法在登录时调用,用于判断一个账号登录session的数量并剔除超出规则的账号。

@Override
public Collection<Session> limitSessions(Object principal){// principal在这个方法指的就是userIDif (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()){// 遍历集合,剔除不规范的内容,一般来说是不会出现的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{// 验证SESSIONsession.validate();sessions.add(session);}// SESSION验证失败catch (Exception e2) {jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());}}// 存储的SESSION不符合规则else{jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());}}// 存储的SESSION无Valueelse if (StringUtils.isNotBlank(e.getKey())){jedis.hdel(sessionUserKeyPrefix + principal, e.getKey());}}// 剔除过期的session后得到的 sessions.size() 才是当前账号所存活的sessionlogger.info("该账户 session 数量: {} ", sessions.size());// 我定义的规则:如果存活的session大于某个值,就对sessions进行时间排序,并且剔除最后操作较早的sessionif(sessions.size() > SESSIONLIMTI) {sessions.sort(new Comparator<Session>() {@Overridepublic 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认证的回调函数,重写内容一般有登录校验、登录日志之类,在这里就可以追加限制登录数量和剔除session的操作,也就是调用前面编写的方法。

/*** 认证回调函数, 登录时调用*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {UsernamePasswordToken token = (UsernamePasswordToken) authcToken;// 校验登录验证码
//业务校验。。。。。。省略// 校验用户名密码以及账号是否冻结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(), "系统登录", user);// 踢人int limitSessionSize = getSystemService().getSessionDao().limitSessions(user.getId()).size();}return new SimpleAuthenticationInfo(principal, 。。。);} else {return null;}
}

新增 ApiLogoutFilter

重写 preHandle 方法,如果退出登录,就从map中移除该session,我本来是打算写在 JedisSessionDAOdelete方法中,但是执行到这个方法的时候已经清除了用户信息,所以无法获得userId,当然可以采用再设置一个sessionId所对应的redis存储辅助,有些冗余,可能有更好的切入点写入,我目前是写在这里。

public class ApiLogoutFilter extends LogoutFilter {private static final Logger log = LoggerFactory.getLogger(ApiLogoutFilter.class);private String sessionUserKeyPrefix = "jes_map:";@Overrideprotected 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;}/** 现在项目基本前后端分离 这一段基本没用HttpServletRequest request = Servlets.getRequest();if (request != null){String uri = request.getServletPath();// 如果是静态文件,则不更新SESSIONif (Servlets.isStaticFile(uri)){return;}// 如果是视图文件,则不更新SESSIONif (StringUtils.startsWith(uri, Global.getConfig("web.view.prefix"))&& StringUtils.endsWith(uri, Global.getConfig("web.view.suffix"))){return;}// 手动控制不更新SESSIONif (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);}
}

最后

在此,我只是规定了固定数量规则,这个限制登录数量当然可以是存储于关系型数据库里和账号绑定的,甚至可以是花里胡哨的规则,例如——手机登录限制只能登录1个,浏览器登录限制10个。还可以通过ws推送,主动告知被剔除的那个客户端——您的账号在福建省XX市XX登录,您被踢下线,如有异常,申请冻结账号。甚至可以列出登录设备列表,让客户可以选择性的剔除哪个设备。只要在map里存储的时间戳修改为这些丰富的数据,就能实现这些很有趣的功能。


http://chatgpt.dhexx.cn/article/uj0nIFX0.shtml

相关文章

SAP SM04踢人操作(摘录)

管理员在SAP系统中&#xff0c;使用事物码SM04对当前登录在线用户&#xff08;User&#xff09;进行管理&#xff0c;可查看服务器全部客户端&#xff08;Client&#xff09;的用户的在线状态&#xff0c;并可以结束指定用户的会话状态&#xff0c;也就是强制踢出用户。 双击查…

出生率新低!1978-2020中国人口出生率、死亡率及自然增长率变迁

根据国家统计局近日发布的《中国统计年鉴2021》&#xff0c;2020中国人口出生率为8.52‰&#xff0c;再创历史新低。同时&#xff0c;2020中国人口自然增长率仅为1.45‰&#xff0c;勉强维持正增长&#xff0c;同样是历史新低。 下面的视频记录了1978-2020中国人口出生率、死亡…

百度迁徙大数据整理[2020+2019同期]

统筹推进疫情防控和经济社会发展工作是常态化疫情防控阶段促进中国经济恢复的必然选择。基于百度地图迁徙大数据&#xff0c;采用双重差分模型探究不同阶段的疫情防控措施对中国人口流动的影响。结果表明&#xff0c;早期的疫情超常规防控措施有效控制了人口流动&#xff0c;导…

【SQLPlanet】基于迁徙率等指标浅析拍拍贷逾期数据(未完待续)

1、背景介绍 拍拍贷是一家金融科技公司&#xff0c;2007年成立于上海&#xff0c;并在2017年11月10日成功于美国纽交所上市。根据官方消息&#xff0c;截至2018年9月30日&#xff0c;拍拍贷累计成交额已突破1300亿&#xff0c;15-29天及30-59天的逾期率分别为0.83%和1.21%&…

百度迁徙 迁入人口和迁徙规模爬虫

最近做COVID-19相关的课程项目&#xff0c;需要用到省级间人口迁移的数据。笔者参考改进了https://blog.csdn.net/qq_44315987/article/details/104118498 的城市间流动的代码&#xff0c;从百度迁徙爬取了数据&#xff0c;并将数据保存在同一张表内。 迁入人口 # coding:utf…

百度大数据迁徙

我记得&#xff0c;百度地图应该是2018年开始做每年春运出行大数据迁移&#xff0c;2019年加入了国庆的迁移大数据分析。这是纯免费的大数据分析数据&#xff0c;相当有意思&#xff0c;但是不作保存&#xff0c;每年做个记录&#xff0c;应该是件有价值的事。 从整体来看&…

什么是计算迁移

0 1 计算迁移背景 边缘计算将网络边缘上的计算、存储等资源进行有机融合&#xff0c;构建成统一的用户服务平台&#xff0c;按就近服务原则对网络边缘节点任务请求及时响应并有效处理。由于边缘节点能力、资源、带宽、能源等受限&#xff0c;计算迁移便异常重要。计算迁移是边缘…

迁徙数据平台简单介绍

引言 人口迁徙数据是研究人口流动、人口迁徙以及城市发展的重要数据源。近年来&#xff0c;受新冠肺炎疫情影响&#xff0c;以及互联网的快速发展&#xff0c;相关数据受到广泛关注与应用&#xff0c;以人口迁徙数据为基础的科学研究也层出不穷。在这里&#xff0c;就对几种常…

人口迁徙大数据(2019-2020年)

人口迁徙大数据具有极高的研究价值&#xff0c;特别是与城市群、疫情等重要话题结合起来&#xff0c;发表了很多核心期刊&#xff1a; 迁徙数据&#xff08;1&#xff09; 高德地图人口迁徙数据&#xff1a;2020年前6个月的400多万条数据&#xff0c;数据为日数据&#xff0c;…

【迁移学习】分布差异的度量以及迁移学习的统一表征方法

在文本分类中&#xff0c;由于文本数据有其领域特殊性&#xff0c;因此&#xff0c;在一个领域上训练的分类器&#xff0c;不能直接拿来作用到另一个领域上&#xff0c;这就需要用到迁移学习。 迁移学习是机器学习中重要的研究领域&#xff0c;ICML、NIPS、AAAI、ICIR等国际人工…

手把手实操系列|贷后迁徙率模型开发(上篇)

序言&#xff1a; 很多关注番茄风控的老铁们都知道&#xff0c;番茄风控的开篇就是从系统性的贷后评分卡开始的&#xff0c;关于贷后相关的内容&#xff0c;番茄不敢说是元老级别的公众号&#xff0c;但再怎么说也是先行者&#xff0c;之前的文章比如这些经典内容&#xff0c;您…

手把手系列|贷后评分(C)卡模型开发实操(全)

序言&#xff1a; 随着风控精细化的管理&#xff0c;番茄风控也就将现有的内容进一步迭代&#xff0c;更新贷后迁徙率模型的内容&#xff0c;同时也综合了星球社区中同学的一些新需求&#xff0c;给大家梳理了贷后迁徙率模型的文章。 希望对所有的风控人员在贷后相关的模型开发…

催收评分卡(三)迁徙率模型

关注公众号“ 番茄风控大数据”&#xff0c;获取更多数据分析与风控大数据的实用干货。 本文主要介绍迁徙率模型和还款率模型&#xff0c;至于失联模型&#xff0c;其实也不难做&#xff0c;难点是在于每家公司对失联客户的定义&#xff0c;主要是需要把多长失时间内失联的客户…

风险资产常用指标

目录 1.放款本金与本金余额 2.迁徙率 3.Vintage 4.不良率 5.损失率 6.回收率 7.入催率 8.首逾率 1.放款本金与本金余额 放款本金为每个月的放贷金额&#xff0c;本金余额为截止统计时点&#xff0c;所有未收回的本金金额&#xff0c;包括逾期未还本金与未到还款期限的待还本金…

前端数据分页——table表数据分页方法(1)

我们在页面展示一个table表格的时候&#xff0c;当数据量较大时&#xff0c;常常会考虑到数据分页的问题&#xff0c;数据分页一般有三种方式&#xff0c;分别是前端数据分页&#xff0c;后端数据分页&#xff0c;数据库分页。 前端数据分页&#xff1a;是把所有数据加载到前端…

利用vue实现树表格分页

目录 1. 准备工作 2. 动态树 2.1 在配置请求路径 2.2 使用动态数据构建导航菜单 2.2.1 通过接口获取数据 2.2.3 通过后台获取的数据构建菜单导航 2.3 点击菜单实现路由跳转 2.3.1 创建书本管理组件 2.3.2 配置路由 2.3.3 修改LeftAside组件 2.3.4 修改Main组件 3. …

Layui 表格分页控件

分页模块 – layui.laypage layPage 致力于提供极致的分页逻辑,既可轻松胜任异步分页,也可作为页面刷新式分页。 快速使用 laypage 的使用非常简单,指向一个用于存放分页的容器,通过服务端得到一些初始值,即可完成分页渲染: 基础参数选项 通过核心方法:laypage.rende…

ant design pro表格分页

需要用到表格的配置项pagination pagination{{ showQuickJumper:true, showTotal:function(total, range){ return( 共total条 ) } }} 效果&#xff1a; 这样可以选择跳转到某个页面&#xff0c;如果如数的数值大于最后一页&#xff0c;那么将跳转到最后一页&#xf…

Thinkphp+layui数据表格实现表格分页

项目场景&#xff1a;Thinkphplayui数据表格实现表格分页 提示&#xff1a;thinkphplayui数据表格分页&#xff0c;需要服务器端配合。之前在百度上一通乱搜也没找到解决方案。 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 之前一看layui的文档一只以…

Vue+elementUI实现Table表格分页效果

VueelementUI实现Table表格分页效果 在毕业项目书写时&#xff0c;需要实现分页效果&#xff0c;而查找了大部分资料&#xff0c;大多是静态Table表格的分页&#xff0c;而该项目的数据大多是后台查询后的动态数据&#xff0c;于是我参考了网上的方法&#xff0c;并进行了一系列…