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

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

相关推荐
cat三三13 分钟前
java之异常
java·开发语言
浙江第二深情18 分钟前
前端性能优化终极指南
java·maven
养乐多072234 分钟前
【Java】IO流
java
俊男无期34 分钟前
超效率工作法
java·前端·数据库
中国胖子风清扬38 分钟前
SpringAI和 Langchain4j等 AI 框架之间的差异和开发经验
java·数据库·人工智能·spring boot·spring cloud·ai·langchain
月明长歌1 小时前
【码道初阶】牛客TSINGK110:二叉树遍历(较难)如何根据“扩展先序遍历”构建二叉树?
java·数据结构·算法
用户2190326527351 小时前
Spring Boot + Redis 注解极简教程:5分钟搞定CRUD操作
java·后端
Alice1 小时前
linux scripts
java·linux·服务器
Filotimo_1 小时前
Spring Data JPA 方法名查询特性的使用
java·开发语言·windows
代码栈上的思考1 小时前
MyBatis:注解方式实现数据库 CRUD 全操作
java·开发语言·mybatis