spring-Session升级之坑

项目场景:

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


问题描述

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

c 复制代码
((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方法,具体代码如下

java 复制代码
    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 :

java 复制代码
  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 逻辑如下

java 复制代码
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 代码如下

java 复制代码
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的逻辑为

java 复制代码
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:问题就得以解决了

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

相关推荐
Abladol-aj36 分钟前
并发和并行的基础知识
java·linux·windows
清水白石00836 分钟前
从一个“支付状态不一致“的bug,看大型分布式系统的“隐藏杀机“
java·数据库·bug
吾日三省吾码6 小时前
JVM 性能调优
java
弗拉唐7 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi778 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3438 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀8 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20208 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深8 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++