项目场景:
因为某些组件低版本存在漏洞问题,本次对项目的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 进入到SessionRepositoryFilter
中getSession
方法,具体代码如下
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:
问题就得以解决了
创作不易,望各位铁汁点赞收藏!谢谢!谢谢!