• shiro+redis环境中session错乱问题


    在shiro+redis环境中使用RedisSessionDAO 操作session遇到的session错乱的问题

    1.       问题描述

    环境为Spring boot的项目中使用shiro框架(Shiro-Core 为1.6版本)作为会话管理,session存储在redis中,redisSession操作使用的是org.crazycake的shiro-redis。系统登录页面login(),输入用户名、密码,验证成功后进入到默认首页,然后马上点击任一菜单之后,偶尔会发生退回到登录页面的情况。

    2.       分析过程

    该情况只在启用session存储在redis的情况下会发生,所以分析和redis的存储或读取有关系,因为该情况偶尔会发生,也没有什么规律,只能采用记录日志的方式。

    分析日志,发现是在shiro过滤器判断用户是否登录时,判断当前请求未登录而导致退出。

    Subject subject = SecurityUtils.getSubject(httpServletRequest,httpServletResponse);
    
            if (!subject.isAuthenticated() /*&& !subject.isRemembered()*/) {//没有登录的情况
    
                if (httpServletRequest.getHeader("x-requested-with") != null && "XMLHttpRequest".equalsIgnoreCase(httpServletRequest.getHeader("x-requested-with"))) {
    
                    httpServletResponse.setHeader("sessionstatus", "timeout");
    
                    return false;
    
                } else {
    
                    String referer = httpServletRequest.getHeader("Referer");
    
                    if (referer == null) {
    
                        jumpToPage(request, response,"未登录");
    
                        return false;
    
                    } else if (ShiroKit.getSession().getAttribute("sessionFlag") == null) {
    
                        
    
                        logger.error("174 subject:{}",subject.toString());
    
                        logger.info(httpServletRequest.getServletPath());
    
                        logger.info("174 session-id:" +ShiroKit.getSession().getId().toString());
    
                        Collection<Object> attributeKeys = ShiroKit.getSession().getAttributeKeys();
    
                        for (Object object:
    
                        attributeKeys) {
    
                            logger.info("174 session-content:" +object);
    
                        }
    
      
    
                        request.setAttribute("tips", ShiroKit.getSession().getAttribute("tips"));
    
                        forward(request,response,"174");
    
                        return false;
    
                    } else {
    
                        jumpToPage(request, response,"未登录");
    
                        return false;
    
                    }
    
                }
    
            }
     


    第一就是怀疑是读取cookie创建sessionid的时候有问题,但是根据打出的日志内容,发现此时传递的cookie和创建的sessionid都是正常的。然后怀疑session读取有问题,但是打出的日志也看不出,下面记录了如何RedisSessionDAO打印出日志。 

    而org.crazycake的shiro-redis中的RedisSessionDAO类,只记录一些异常情况,所以新建一个SessionDAO类,在shiroConfig中进行配置

    @Bean
    @ConditionalOnProperty(
            prefix = "global",
            name = {"stand-alone"},
            havingValue = "false",
            matchIfMissing = false
    )
    public SessionDAO redisSessionDAO(IRedisManager redisManager) {
        SessionDAO sessionDAO = null;
       /* sessionDAO = new RedisSessionDAO();
        ((RedisSessionDAO) sessionDAO).setRedisManager(redisManager);*/
        sessionDAO = new MyRedisSessionDAO();
        ((MyRedisSessionDAO) sessionDAO).setRedisManager(redisManager);
        return sessionDAO;
    }
    
     
    @Bean
    
        @ConditionalOnProperty(
    
                prefix = "global",
    
                name = {"spring-session-open"},
    
                havingValue = "false"
    
        )
    
        public DefaultWebSessionManager defaultWebSessionManager(CacheManager cacheShiroManager, Collection<SessionListener> listeners, SessionDAO sessionDAO) {
    
      
    
            DefaultWebSessionManager sessionManager = new AdminWebSessionManager();
    
            sessionManager.setSessionValidationScheduler(this.sessionValidationScheduler(sessionManager));
    
            sessionManager.setSessionValidationInterval((long) (this. Properties.getSessionValidationInterval() * this.kilo));
    
            sessionManager.setGlobalSessionTimeout((long) (this. Properties.getSessionInvalidateTime() * this.kilo));
    
            sessionManager.setDeleteInvalidSessions(true);
    
            sessionManager.setSessionValidationSchedulerEnabled(true);
    
            sessionManager.setSessionIdUrlRewritingEnabled(false);
    
            sessionManager.setSessionListeners(listeners);
    
            sessionManager.setCacheManager(cacheShiroManager);
    
            sessionManager.setSessionDAO(sessionDAO); 
    
            sessionManager.setSessionIdCookieEnabled(true);
    
            Cookie cookie = new SimpleCookie(this.globalProperties.getTitle() + "_cookie");
    
            cookie.setHttpOnly(true);
    
            sessionManager.setSessionIdCookie(cookie);
    
      
    
            return sessionManager;
    
        }

    新建的类MyRedisSessionDAO的读取session代码修改如下:

    protected Session doReadSession(Serializable sessionId) {
    
        if (sessionId == null) {
    
            logger.warn("session id is null");
    
            return null;
    
        } else {
    
            Session session;
    
            if (this.sessionInMemoryEnabled) {
    
                session = this.getSessionFromThreadLocal(sessionId);//从当前线程的threadlocal中获取session
    
                logger.info("read session from memory");
    
                if (session != null) {
    
                    return session;
    
                }
    
            }
    
      
    
            session = null;
    
            logger.info("read session from redis");
    
      
    
            try {
    
                String content = "";
    
                byte[] bytes = this.redisManager.get(this.keySerializer.serialize(this.getRedisSessionKey(sessionId)));
    
                if(bytes != null){
    
                    content = new String(bytes);
    
                }
    
      
    
                session = (Session)this.valueSerializer.deserialize(this.redisManager.get(this.keySerializer.serialize(this.getRedisSessionKey(sessionId))));
    
                logger.info("session's content is :" +content);
    
                if (this.sessionInMemoryEnabled) {
    
                    this.setSessionToThreadLocal(sessionId, session);
    
                }
    
            } catch (SerializationException var4) {
    
                logger.error("read session error. settionId=" + sessionId);
    
            }
    
      
    
            return session;
    
        }
    
    }

    分析SessionDAO读取session的逻辑,RedisSessionDAO的设计中,为了避免频繁的读取redis,默认设置了1000ms时间范围内先在当前线程的ThreadLocal中获取,如果没有则再读取redis,读取后再写到当前线程的ThreadLocal中。感觉这里有可能会在某些情况下有问题。

    再往下分析日志,通过在shiro的判断用户是否登录的过滤器中打印的日志,发现出现问题的时候,处理url地址为/login请求方式为get的线程和登录成功后点击某个菜单的线程名称一样,同为“http-nio-exec-18”,而且此时异常退出的url,其日志打印出的session中的内容和url地址为/login请求方式为get的线程的session相同,那么就怀疑是使用了url地址为/login请求方式为get的线程,而url地址为/login请求方式为get的线程没有释放threadlocal中的内容所导致的问题。

    3.       结论

    发生问题的具体流程

    一.  用户访问登录页面,地址为/login,请求方式为get,Shiro框架为其分配一个sessionid,假如为1,存储在cookie中,此时后端服务器也在redis中存储了sessionid为1的session对象,同时由于redisSessionDao考虑频繁读取redis的原因,还将该session对象存储到当前request线程的threadlocal中,此时的session对象没有用户的相关信息

    二.  用户输入用户名、密码,验证成功后,将sessionid为1的session对象存储到redis中(替换之前存储在redis中的sessionid为1的对象),此时的session对象已经包含用户的相关信息,标识已经登录,并将该对象放到当前request线程的threadlocal中,然后跳转到默认首页,先执行shiro的判断用户是否登陆的过滤器代码,该过滤器判断当前subject的session是否应经登录,此时如果分配的不是第一步的处理url为/login,请求方式为get的线程,那么过滤器判断已经登录,放行到首页。

    三.  用户立即点击某个菜单,访问一个url地址,tomcat从线程池中为当前请求分配一个线程,此时刚好分配了之前处理url地址为/login、请求方式为get的线程;此时再次执行shiro的过滤器判断当前subject是否登录,而subject获取session的代码就是先判断当前线程的threadlocal中是否有sessionid为1的session对象,由于当前线程就是刚刚处理url地址为/login、请求方式为get的线程,并且sessionid也是相同(登录前login页面分配的sessionid和登录后的sessionid始终都是相同的),所以刚好能从当前线程中取到session对象,也就不会再去redis中取session对象(redis中的session对象是正确的),但是该session对象是不包含用户登录信息的,所以过滤器中的逻辑就是判断用户没有登录,就退出到登录页面了。

    4.       解决方法

    新建一个过滤器,该过滤器优先级最高(职责链上第一个执行,最后一个退出),该过滤器在职责链最后将当前线程的threadlocal清除掉。代码如下:

    import javax.servlet.*;
    
      import java.io.IOException;
    
      
    
      public class RemoveShiroThreadContextFilter implements Filter {
    
        private static Logger LOGGER = LoggerFactory.getLogger(RemoveShiroThreadContextFilter.class);
    
        @Override
    
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
            try {
    
                filterChain.doFilter(servletRequest, servletResponse);
    
            }
    
            finally {
    
                ThreadContext.remove();
    
            }
    
      
    
        }
    
    }

    WebConfig中的配置:

    @Bean
    
      public FilterRegistrationBean<RemoveShiroThreadContextFilter> shiroThreadFilterRegistration() {
    
        RemoveShiroThreadContextFilter shiroThreadContextFilter = new RemoveShiroThreadContextFilter();
    
      
    
        FilterRegistrationBean<RemoveShiroThreadContextFilter> registration = new FilterRegistrationBean(shiroThreadContextFilter, new ServletRegistrationBean[0]);
    
        registration.addUrlPatterns(new String[]{"/*"});
    
        registration.setOrder(-100);
    
        return registration;
    
    }

     参考文章:

    Shiro在多线程环境中

    线程池shiro获取当前user出错问题,及解决方案 

    netty整合shiro,报There is no session with id [xxxxxx]问题定位及解决

    session 莫名丢失

  • 相关阅读:
    uva10912 Simple Minded Hashing(DP)
    uva10401 Injured Queen Problem(DP)
    uva702 The Vindictive Coach(DP)
    忍者X4将采取自动开通vip,论坛充值淘宝自助购买均可。步骤如下
    C盘不够大,可以这样操作
    任务思维1
    PHP 获取指定日期的星期方法如下
    学学C#开发client,server,C/S架构的程序
    今天的主角就是protobuf-net
    关于忍者站群X4-小飞镖服务器配置帮助汇总。
  • 原文地址:https://www.cnblogs.com/bayu/p/14115753.html
Copyright © 2020-2023  润新知