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 莫名丟失


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM