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:问题就得以解决了

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

相关推荐
【D'accumulation】10 分钟前
典型的MVC设计模式:使用JSP和JavaBean相结合的方式来动态生成网页内容典型的MVC设计模式
java·设计模式·mvc
试行24 分钟前
Android实现自定义下拉列表绑定数据
android·java
茜茜西西CeCe30 分钟前
移动技术开发:简单计算器界面
java·gitee·安卓·android-studio·移动技术开发·原生安卓开发
救救孩子把35 分钟前
Java基础之IO流
java·开发语言
小菜yh36 分钟前
关于Redis
java·数据库·spring boot·redis·spring·缓存
宇卿.43 分钟前
Java键盘输入语句
java·开发语言
浅念同学43 分钟前
算法.图论-并查集上
java·算法·图论
立志成为coding大牛的菜鸟.1 小时前
力扣1143-最长公共子序列(Java详细题解)
java·算法·leetcode
鱼跃鹰飞1 小时前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
爱上语文2 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring