文档章节

生产环境频繁被自动退出

Mr_Qi
 Mr_Qi
发布于 2017/06/02 13:27
字数 1378
阅读 24
收藏 0

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

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

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

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

那么用户登陆了两次会有啥问题呢?

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

用户当登录成功后

会调用AuthenticationListener的onSuccess回调。

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

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

主要问题在此

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

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

那么改善呢也很简单,对应的根据redisKey来做一把锁(分布式情况较为复杂)恰巧我放系统正是分布式系统。那么如何解决呢?

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

改善后代码如下

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

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

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

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

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

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

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

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

分析一下

不带有sessionid

req1过来系统分配sessionId1

req2过来系统分配sessionId2

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

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

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

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

带有sessionid

req1过来系统使用sessionId1

req2过来系统使用sessionId1

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

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

req1返回画面给浏览器 请求结束(将sessionid1写入到cookie)

没问题可以正常使用

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

修改对应代码 问题解决。

© 著作权归作者所有

共有 人打赏支持
Mr_Qi
粉丝 264
博文 326
码字总数 341201
作品 0
南京
程序员
我们应该如何基于容器来进行软件的持续交付(二)?

概述 接着上一篇的内容,我们有讲到“持续交付是文化,自动化是基石,垮职能团队协作是根本”,本文将以软文的形式介绍持续交付平台WiseBuild结合Rancher容器管理平台我们是如何进行跨职能团...

wise2c
2016/12/26
85
0
我们应该如何基于容器来进行软件的持续交付(一)?

概述 在过去的一段时间里容器已经大量的使用到了IT软件生产的各个环节当中:从软件开发,持续集成,持续部署,测试环境到生产环境。 除了Docker官方的Docker Swarm, Docker Machine以及Docke...

wise2c
2016/12/22
163
0
谈谈持续集成,持续交付,持续部署之间的区别

经常会听到持续集成,持续交付,持续部署,三者究竟是什么,有何联系和区别呢? 假如把开发工作流程分为以下几个阶段: 编码 -> 构建 -> 集成 -> 测试 -> 交付 -> 部署 正如你在上图中看到,...

风起云飞fir_im
2016/08/04
46
0
CI持续集成介绍

互联网软件的开发和发布,已经形成了一套标准流程,最重要的组成部分就是持续集成(Continuous integration,简称CI)。 一、概念 持续集成(Continuous Integration)指的是,频繁地(一天多...

若与
2017/11/26
0
0
持续交付:发布可靠软件的系统方法

1 持续交付和持续部署 持续交付 是目前的一个挺火的概念,它所描述的软件开发,是从原始需求识别到 最终产品部署到生产环境这个过程中,需求以小批量形式在团队的各个角色间顺 畅流动,能够以...

onlyzq
2015/03/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

1、Vue解决安卓4.4不兼容的问题

1.npm安装 npm install babel-polyfill--save-devnpm install es6-promise--save-dev package.json中会出现 "babel-polyfill": "^6.26.0","es6-promise": "^4.1.1", 2.main.js引入 impo......

阿K1225
13分钟前
0
0
mybatis generator 属性详解

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis......

太黑_thj
16分钟前
1
0
windows nvm 安装 node

nvm 是 node 的版本控制管理 下面是下载 nvm 的地址,选择 nvm-setup.zip 下载 https://github.com/coreybutler/nvm-windows/releases 就是下一步下一步,一键安装 基本命令有: nvm arch [32...

U_I_A_N
18分钟前
0
0
js判断字符串中是否包含某个字符串

indexOf() indexof()方法可以返回某个指定的字符串值在字符串中首次出现的位置。如果要检索的字符串值没有出现,则该方法返回-1。 var str = "123";console.log(str.indexOf("3") != -1);...

小星星_cjx
31分钟前
0
0
函数式组件完整例子

之前创建的组件是比较简单,没有管理或者监听任何传递给他的状态,也没有生命周期方法。它只是一个接收参数的函数。 在下面这个例子中,我们标记组件为 functional,这意味它是无状态 (没有响...

tianyawhl
33分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部