热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

生产环境频繁被自动退出

title:生产环境频繁被自动退出tags:shiroredislua事务kickoutcategories:工作日志date:2017-05-2518:18:56最近发现一个奇怪

title: 生产环境频繁被自动退出 tags:

  • shiro
  • redis
  • lua
  • 事务
  • kickout categories: 工作日志 date: 2017-05-25 18:18:56

最近发现一个奇怪的现象,用户登录的时候总是提示

本以为只是正常偶发,突然最近日志

check了一下登录请求发现如下

192.168.1.170 - - [01/Jun/2017:10:39:10 +0800] "GET /kzf6/user/login.do?username=lihuaqixiu&password=96e79218965eb72c92a549dd5a330112 HTTP/1.1" 200 226
192.168.1.170 - - [01/Jun/2017:10:39:11 +0800] "GET /kzf6/user/login.do?username=lihuaqixiu&password=96e79218965eb72c92a549dd5a330112 HTTP/1.1" 200 226

基本上通过按回车登录的用户均是会出现两条请求&#xff08;即用户登录了两次<几乎同时>&#xff09;

那么用户登陆了两次会有啥问题呢&#xff1f;

参考shiro实现用户踢出功能 实现

用户当登录成功后

会调用AuthenticationListener的onSuccess回调。

此时会校验当前用户是否有其他的sessionId存在&#xff0c;如果存在就按照踢出策略让对应session标记成被踢出&#xff0c;当回话再次访问时会直接跳到被踢出画面。

其实无论开涛或者我修改后的版本都存在一个问题&#xff0c;同步。

主要问题在此

while (lop.size(redisListKey) > maxSession) { Serializable kickoutSessionId;if (kickoutAfter) { //如果踢出后者kickoutSessionId &#61; lop.rightPop(redisListKey);} else { //否则踢出前者kickoutSessionId &#61; lop.leftPop(redisListKey);}

其实说起来也很明显&#xff0c;查询和做修改的操作并不是同步的&#xff0c;比如对于同一个redisKey来说&#xff08;并发&#xff09;即很有可能出现意料之外的问题。

那么改善呢也很简单&#xff0c;对应的根据redisKey来做一把锁&#xff08;分布式情况较为复杂&#xff09;恰巧我放系统正是分布式系统。那么如何解决呢&#xff1f;

和原先一样&#xff0c;使用redis的lua脚本来完成 参考 shiro实现用户踢出功能

改善后代码如下

---- Created by IntelliJ IDEA.-- User: qixiaobo-- Date: 2017/6/2-- Time: 10:24-- 移除session id&#xff0c;当sessionid数目小于允许登录数这返回空&#xff0c;使用lua脚本redis操作的保证原子性-- keys[1]对应redis的list的key-- args[1]对应maxSession-- args[2]对应 true:left 或者false: right-- 返回是否存入redislocal list_key &#61; KEYS[1];local max_session &#61; ARGV[1];local remove_before &#61; ARGV[2]local size &#61; redis.call(&#39;LLEN&#39;, list_key);local session_id;if size > tonumber(max_session) thenif remove_before &#61;&#61; &#39;true&#39; thensession_id &#61; redis.call(&#39;LPOP&#39;, list_key);elsesession_id &#61; redis.call(&#39;RPOP&#39;, list_key);end;end;return session_id;

<bean id&#61;"removeSessionKey" class&#61;"org.springframework.data.redis.core.script.DefaultRedisScript"><property name&#61;"location" value&#61;"classpath:removeSessionKey.lua"/><property name&#61;"resultType" value&#61;"java.lang.String"/>bean>

/*** Created by qixiaobo on 2017/5/22.*/public class KickOutSessionListener implements AuthenticationListener {private static final String SESSION_KEY_KICKOUT &#61; "kickout";private static final String SESSION_KEY_KICKOUT_TIME &#61; "kickout_time";private static final String SESSION_KEY_KICKOUT_IP &#61; "kickout_ip";private static final String REDIS_KEY_PREFIX &#61; CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME &#43; ":";private Logger logger &#61; LoggerFactory.getLogger(KickOutSessionListener.class);private boolean kickoutAfter; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户private int maxSession; //同一个帐号最大会话数 默认1private SessionManager sessionManager;&#64;Autowired&#64;Qualifier(value &#61; "stringRedisTemplate")private StringRedisTemplate template;&#64;Autowiredprivate RedisScript addSessionAndExpireList;&#64;Autowiredprivate RedisScript removeSessionKey;&#64;Value("#{T(java.lang.String).valueOf(${session.validation.interval}/1000)}")private String sessionExpire;&#64;Value("${shiro.kickout}")private boolean enable;&#64;Overridepublic void onSuccess(AuthenticationToken token, AuthenticationInfo info) {if (enable) {Subject subject &#61; SecurityUtils.getSubject();HttpServletRequest request &#61; WebUtils.getHttpRequest(subject);Session session;final String username &#61; token.getPrincipal().toString();try {session &#61; subject.getSession();} catch (SessionException ex) {logger.warn(ex.getMessage(), ex);return;}if (session &#61;&#61; null) {return;}String sessionId &#61; (String) session.getId();ListOperations lop &#61; template.opsForList();final String redisListKey &#61; getRedisKey(username);//通常情况下 maxSession为1就不判断size了try {List listKey &#61; Collections.singletonList(redisListKey);if (session.getAttribute(SESSION_KEY_KICKOUT) &#61;&#61; null) {template.execute(addSessionAndExpireList, listKey, sessionId, sessionExpire);}//如果队列里的sessionId数超出最大会话数&#xff0c;开始踢人String kickoutSessionId;while ((kickoutSessionId &#61; template.execute(removeSessionKey, listKey, String.valueOf(maxSession), String.valueOf(!kickoutAfter))) !&#61; null) {Session kickoutSession;try {kickoutSession &#61; sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));} catch (SessionException exception) {logger.warn(exception.getMessage(), exception);kickoutSession &#61; null;}if (kickoutSession !&#61; null) {//设置会话的kickout属性表示踢出了kickoutSession.setAttribute(SESSION_KEY_KICKOUT, true);kickoutSession.setAttribute(SESSION_KEY_KICKOUT_TIME, new DateTime().toString(AppConstant.DEFAULT_DATE_FORMAT_PATTERN));kickoutSession.setAttribute(SESSION_KEY_KICKOUT_IP, WxbStatic.getRemoteIp(request));}}} catch (Exception ex) {logger.error(ex.getMessage(), ex);}}}&#64;Overridepublic void onFailure(AuthenticationToken token, AuthenticationException ae) {}&#64;Overridepublic void onLogout(PrincipalCollection principals) {}public void setKickoutAfter(boolean kickoutAfter) {this.kickoutAfter &#61; kickoutAfter;}public void setMaxSession(int maxSession) {this.maxSession &#61; maxSession;}public void setSessionManager(SessionManager sessionManager) {this.sessionManager &#61; sessionManager;}public StringRedisTemplate getTemplate() {return template;}public void setTemplate(StringRedisTemplate template) {this.template &#61; template;}private String getRedisKey(String key) {return REDIS_KEY_PREFIX &#43; key;}}

核心改动就是将redis list的查询和修改放在了lua脚本中完成&#xff0c;维护了事务性。

问题回到原点&#xff0c;上述改动是对于事务做了改善&#xff0c;那么为何会出现用户被强制退出了呢&#xff1f;

经过调查发现 基本上用户早上过来会出现一波高峰被强制退出。

考虑可能和session过期时间有关&#xff0c;我们配置session的过期时间为180min&#xff0c;那么在用户早上过来我们可有认为是新会话&#xff08;没有对应的sessionid&#xff09;。

由于某种方式登录时如同开头时所说存在发起了两遍登录的请求&#xff08;这个bug太cheap了&#xff09;

分析一下

不带有sessionid

req1过来系统分配sessionId1

req2过来系统分配sessionId2

req1经过kickoutlistener 发生如下行为 将sessionId1放入list 同时校验sessionid1没问题 不会标记

req2经过kickoutlistener 发生如下行为 将sessionId2放入list 同时校验sessionid2没问题 不会标记 但是将sessionid1标记为退出

req1返回画面给浏览器 请求结束&#xff08;将sessionid1写入到COOKIE&#xff09;如果画面足够快的话此时还没有接收到req2的返回&#xff08;就不会有req2的session2回写到COOKIE&#xff09;

点击任何画面会自动标记成被踢出

带有sessionid

req1过来系统使用sessionId1

req2过来系统使用sessionId1

req1经过kickoutlistener 发生如下行为 将sessionId1放入list 如果存在就不放入否则放入

req2经过kickoutlistener 发生如下行为 将sessionId2放入list 如果存在就不放入否则放入

req1返回画面给浏览器 请求结束&#xff08;将sessionid1写入到COOKIE&#xff09;

没问题可以正常使用

因此问题的根本原因是一个没有登陆过或者已经过期或者清楚过所有COOKIE的同时登陆了两次系统造成。

修改对应代码 问题解决。



推荐阅读
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文讨论了如何在codeigniter中识别来自angularjs的请求,并提供了两种方法的代码示例。作者尝试了$this->input->is_ajax_request()和自定义函数is_ajax(),但都没有成功。最后,作者展示了一个ajax请求的示例代码。 ... [详细]
  • iOS Swift中如何实现自动登录?
    本文介绍了在iOS Swift中如何实现自动登录的方法,包括使用故事板、SWRevealViewController等技术,以及解决用户注销后重新登录自动跳转到主页的问题。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • RouterOS 5.16软路由安装图解教程
    本文介绍了如何安装RouterOS 5.16软路由系统,包括系统要求、安装步骤和登录方式。同时提供了详细的图解教程,方便读者进行操作。 ... [详细]
  • 本文介绍了Oracle存储过程的基本语法和写法示例,同时还介绍了已命名的系统异常的产生原因。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • REVERT权限切换的操作步骤和注意事项
    本文介绍了在SQL Server中进行REVERT权限切换的操作步骤和注意事项。首先登录到SQL Server,其中包括一个具有很小权限的普通用户和一个系统管理员角色中的成员。然后通过添加Windows登录到SQL Server,并将其添加到AdventureWorks数据库中的用户列表中。最后通过REVERT命令切换权限。在操作过程中需要注意的是,确保登录名和数据库名的正确性,并遵循安全措施,以防止权限泄露和数据损坏。 ... [详细]
author-avatar
夏山_Els乀i丷e
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有