spring-session升级之坑

项目场景:

因为某些组件低版本存在漏洞问题,本次对项目的springboot版本从1.x升级到了2.x,因为其他相关的中间件也随着一起升级,在升级最后发现项目用户信息无法获取到了。


问题描述

接口获取用户信息报错,获取用户信息是通过spring-session-data-redis 中间件进行处理的。升级前spring-session的版本是1.3,升级到2.x之后就获取不到用户信息了。
问题代码:

((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession()

原因分析:

当然问题代码我们定位到了,是获取不到session,因为使用了spring-session中间件,因此问题肯定就出在从redis中获取失败了。(因为保存的用户信息是在另一个项目,这个项目是没有动的,所以我们能明确是从redis中获取用户信息失败了)

先说源码跟踪结论:
版本升级前的key生成逻辑为: “spring:session:” + namespace + “:”+“sessions:” + sessionId
升级后的key生成逻辑为:namespace + “:”+“sessions:” + sessionId

切换到版本升级前(spring-session 1.3),梳理redis获取用户信息逻辑:
debug getSession 进入到SessionRepositoryFiltergetSession方法,具体代码如下

    public SessionRepositoryFilter<S>..SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
      SessionRepositoryFilter<S>..SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
      if (currentSession != null) {
        return currentSession;
      } else {
      	//获取sessionId,继续debug深入,会发现本项目使用的是HeaderHttpSessionStrategy实现类,配置的是header中的token作为requestedSessionId
        String requestedSessionId = this.getRequestedSessionId();
        ExpiringSession session;
        if (requestedSessionId != null && this.getAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR) == null) {
        // debug本行代码会发现,这个地方就开始从redis获取用户信息了,所以下面一行的代码就非常的关键了
          session = this.getSession(requestedSessionId);
          if (session != null) {
            this.requestedSessionIdValid = true;
            currentSession = new HttpSessionWrapper(session, this.getServletContext());
            currentSession.setNew(false);
            this.setCurrentSession(currentSession);
            return currentSession;
          }

          if (SessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {
            SessionRepositoryFilter.SESSION_LOGGER.debug("No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
          }

          this.setAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");
        }

        if (!create) {
          return null;
        } else {
          if (SessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {
            SessionRepositoryFilter.SESSION_LOGGER.debug("A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + SessionRepositoryFilter.SESSION_LOGGER_NAME, new RuntimeException("For debugging purposes only (not an error)"));
          }

          session = (ExpiringSession)SessionRepositoryFilter.this.sessionRepository.createSession();
          session.setLastAccessedTime(System.currentTimeMillis());
          currentSession = new HttpSessionWrapper(session, this.getServletContext());
          this.setCurrentSession(currentSession);
          return currentSession;
        }
      }
    }

继续深入 session = this.getSession(requestedSessionId);方法,会看到框架是如何拼接key的如果去redis中获取用户信息的。
RedisOperationsSessionRepository.class.BoundHashOperations :

  private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
  	//sessionId 我们通过前面的源码分析出来 是获取的header中的token
  	//在此行才真正生成redis key
    String key = this.getSessionKey(sessionId);
    return this.sessionRedisOperations.boundHashOps(key);
  }
  
	// 包装key的方法  keyPrefix  = "spring:session:" + namespace + ":"
	// redis key  = "spring:session:" + namespace + ":"+"sessions:" + sessionId;
	//通过已知key 中redis
  String getSessionKey(String sessionId) {
    return this.keyPrefix + "sessions:" + sessionId;
  }

升级spring-session 2.7之后

SessionRepositoryFilter.class getSession 逻辑如下

public HttpSessionWrapper getSession(boolean create) {
			HttpSessionWrapper currentSession = getCurrentSession();
			if (currentSession != null) {
				return currentSession;
			}
			//关键代码是本行
			S requestedSession = getRequestedSession();
			if (requestedSession != null) {
				if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
					requestedSession.setLastAccessedTime(Instant.now());
					this.requestedSessionIdValid = true;
					currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
					currentSession.markNotNew();
					setCurrentSession(currentSession);
					return currentSession;
				}
			}
			else {
				// This is an invalid session id. No need to ask again if
				// request.getSession is invoked for the duration of this request
				if (SESSION_LOGGER.isDebugEnabled()) {
					SESSION_LOGGER.debug(
							"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
				}
				setAttribute(INVALID_SESSION_ID_ATTR, "true");
			}
			if (!create) {
				return null;
			}
			if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
					&& this.response.isCommitted()) {
				throw new IllegalStateException("Cannot create a session after the response has been committed");
			}
			if (SESSION_LOGGER.isDebugEnabled()) {
				SESSION_LOGGER.debug(
						"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
								+ SESSION_LOGGER_NAME,
						new RuntimeException("For debugging purposes only (not an error)"));
			}
			S session = SessionRepositoryFilter.this.sessionRepository.createSession();
			session.setLastAccessedTime(Instant.now());
			currentSession = new HttpSessionWrapper(session, getServletContext());
			setCurrentSession(currentSession);
			return currentSession;
		}

getRequestedSession 代码如下

private S getRequestedSession() {
			if (!this.requestedSessionCached) {
				List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
				for (String sessionId : sessionIds) {
					if (this.requestedSessionId == null) {
						this.requestedSessionId = sessionId;
					}
					//本行代码为关键代码,继续debug 会发现框架是如何包装 SessionId 的,此时的SessionId还是header中的token值
					S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
					if (session != null) {
						this.requestedSession = session;
						this.requestedSessionId = sessionId;
						break;
					}
				}
				this.requestedSessionCached = true;
			}
			return this.requestedSession;
		}

继续debug会进入RedisIndexedSessionRepository.class 包装可以的方法如下,得出key的逻辑为

String getSessionKey(String sessionId) {
		return this.namespace + "sessions:" + sessionId;
	}

解决方案:

通过两个版本的源码分析,发现是两个版本生成key的策略发生了变化,1.3版本生成key的策略为:spring:session:" + namespace + ":"+"sessions:" + sessionId 2.7版本生成key的策略为:namespace + ":"+"sessions:" + sessionId
namespace是自定义的,因此升级之后我们把原来的namespace 增加了前缀 spring:session:问题就得以解决了

创作不易,望各位铁汁点赞收藏!谢谢!谢谢!

你可能感兴趣的:(java,开发问题集,spring,java,后端)