文档章节

生产环境频繁被自动退出

Mr_Qi
 Mr_Qi
发布于 2017/06/02 13:27
字数 1378
阅读 27
收藏 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

Mr_Qi

粉丝 280
博文 359
码字总数 369228
作品 0
南京
程序员
私信 提问
生产系统DRDS建表报sequence已经存在问题

1、 问题描述 客户反馈生产环境系统drds创建表报错sequence已经存在,具体报错如下: 但是虽然执行create已经报错,但是表已经有了,并且可以正常的插入数据; 2、 问题排查 排查drds的docke...

silencecxq
09/06
0
0
我们应该如何基于容器来进行软件的持续交付(二)?

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

wise2c
2016/12/26
85
0
App发布流程

简要流程: 发版本之前在测试环境上做好新版本的回归测试、集成测试 同时做好旧版本(若干版本)的兼容性测试 测试环境测试完成后,服务器先发布,在生产环境做兼容性测试 兼容性测试完成以后...

varvelworld
2015/04/29
3
0
你准备好持续交付(CD)了吗?

[toc] 持续交付(CD, Continuous delivery)就是说每次提交代码时立即构建,并可以将构建部署到生产环境中,本文将分享一些持续交付相关的方法和经验。 自动化(Automation) 自动化对于完善...

好雨云帮
10/15
0
0
我们应该如何基于容器来进行软件的持续交付(一)?

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

wise2c
2016/12/22
163
0

没有更多内容

加载失败,请刷新页面

加载更多

聊聊flink的FsStateBackend

序 本文主要研究一下flink的FsStateBackend StateBackend flink-runtime_2.11-1.7.0-sources.jar!/org/apache/flink/runtime/state/StateBackend.java @PublicEvolvingpublic interface Sta......

go4it
22分钟前
0
0
webpack配置proxyTable时pathRewrite无效的解决方法

webpack配置接口地址代理 在项目开发中,接口联调的时候一般都是同域名下,且不存在跨域的情况下进行接口联调,但是当我们在本地启动服务器后,比如本地开发服务下是 http://localhost:8080 ...

前端小攻略
23分钟前
0
0
安装jenkins

1.下载 wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/war/2.155/jenkins.war 2.后续操作和 dubbo 安装类似: (1)复制一份空白的tomcat,重命名为:jenkins-tomcat (2)war包放入t...

狼王黄师傅
31分钟前
1
0
zookeeper配置与使用

一.登录官网下载 不要带后缀的,那是公侧版本,下稳定版,比如3.4.9 二.安装与使用 解压后bin里是启动程序 配置文件:在conf下 复制zoo_sample.cfg改名为为zoo.cfg,打开zoo修改文件 临时数据保存...

小兵胖胖
55分钟前
3
0
spring源码阅读笔记(一)

ClassPathXmlApplicationContext 与 FileSystemXmlApplicationContext 用了这么久的框架,是时候搞一下源码了,一般最初接触spring 从以下步骤开始 创建一个bean类 并创建 ooxx.xml之类的spr...

NotFound403
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部