스프링 세션 Key가 두 개 생성되는 문제와 인코딩 된 값의 비밀
Spring Session을 이용하면서 세션이 두 개 생성되는 문제를 해결하는 과정을 정리해보았습니다.
🔍 Key가 두 개 생성되는 문제
세션을 저장하는 로직은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
class SessionFilter(
private val redisSessionRepository: RedisSessionRepository,
): OncePerRequestFilter() {
val log = logger()
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val session = getSessionFromCookie(request) --- 1
if (session == null) {
val sessionId = request.getSession(true).id --- 2
myRedisSessionRepository.setSession(sessionId) --- 3
}
filterChain.doFilter(request, response)
}
}
- 쿠키에 세션에 대한 정보가 있는지 확인한다.
- 세션이 없다면 새로운 세션을 생성한다.
- 새로 발급받은 세션을 레디스에 저장한다.
결과를 보면 세션이 두 개가 저장되었음을 확인할 수 있습니다. 첫 번째는 위의 코드에 의해 저장된 것이고 2번 세션은 왜 저장된 것일까요?
이를 해결하기 위해 디버깅 모드로 코드를 하나씩 따라 가 보겠습니다.
- getSession()을 호출하면 HttpServletRequestWrapper의 getSession이 호출된다.
1 2 3 4 5 6
class HttpServletRequestWrapper { @Override public HttpSession getSession(boolean create) { return this._getHttpServletRequest().getSession(create); } }
- SessionRepositoryFilter가 동작한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@Override public HttpSessionWrapper getSession(boolean create) { // 1. Session이 있는지 확인합니다. HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } // 2. Session을 생성하고 등록합니다. S session = SessionRepositoryFilter.sessionRepository.createSession(); session.setLastAccessedTime(Instant.now()); currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); return currentSession; }
- RedisSessionRepository가 동작한다.
1 2 3 4 5 6 7 8 9 10
public RedisSessionRepository { @Override public RedisSession createSession() { MapSession cached = new MapSession(); cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval); RedisSession session = new RedisSession(cached, true); session.flushIfRequired(); // return session; } }
여기서 flush모드가 yes라면 곧바로 캐시에 업데이트 되고 no라면 응답을 보내기 전에 DispatcherServlet에서 save() 메서드를 호출하여 저장하는 작업을 거칩니다. 즉, 세션을 새로 발급하면 SessionRepositoryFilter가 동작하여 Redis에 세션을 저장해줍니다.
따라서 request.getSession(true)
를 호출하면 세션을 자동으로 저장해주기 때문에 myRedisSessionRepository.setSession(sessionId)
코드는 필요가 없습니다.
🔍 쿠키와 Base64 인코딩
웹 브라우저에서 확인해보면 SESSION이 인코딩되어서 들어간 것을 확인할 수 있습니다. (세션이 두 개 있는데 이는 다음 글과 관련이 있습니다.)
configuration에서 session 생성을 false로 두면 된다고 하던데 저는 잘 동작 되지 않았습니다. (스프링의 세션 생성 기능을 끈다는 의미이니까 아마 제대로 동작했다면 위의 문제도 같이 해결되었을 것 같네요.)
다음 링크에서 힌트를 얻어 DefaultCookieSerializer부터 디버깅 모드로 하나씩 찾아나갔습니다.
DefaultCookieSerializer - StackOverflow
val sessionId = request.getSession(true).id
다음 코드를 호출하는 순간 RedisSessionRepository에서 세션을 생성하고 이를 현재 세션으로 넣은 다음 헤더에 추가해줍니다. 즉, 추가적으로 header에 담는 로직이 필요가 없습니다. 실제로 세션이 2개가 들어왔지만 하나는 base64로 인코딩 한 값입니다.
이를 추적하면서 뽑아낸 중요한 코드 리스트입니다.
- Session이 있는지 확인한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
@Override public HttpSessionWrapper getSession(boolean create) { HttpSessionWrapper currentSession = getCurrentSession(); // 현재 세션이 존재하면 return 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; } } // create의 속성이 fasle라면 세션을 생성하지 않는다. if (!create) { return null; } // create = true라면 새로운 세션을 생성한다. S session = SessionRepositoryFilter.this.sessionRepository.createSession(); session.setLastAccessedTime(Instant.now()); currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); return currentSession; }
- 지정한 스토리지에서 Session을 생성한다. (flush모드에 따라 저장 시점이 다르다)
1 2 3 4 5 6 7 8
@Override public RedisSession createSession() { MapSession cached = new MapSession(); cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval); RedisSession session = new RedisSession(cached, true); session.flushIfRequired(); return session; }
- 이후 Response 보내기 전에 세션을 Base64로 인코딩하여 header에 저장한다.
1 2 3 4 5 6 7 8
@Override public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) { if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) { return; } request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId); this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId)); }
1 2 3 4 5 6
@Override public void writeCookieValue(CookieValue cookieValue) { String value = getValue(cookieValue); // getValue()에서 인코딩함. ... response.addHeader("Set-Cookie", sb.toString()); // 헤더에 저장 }
결론
- 스프링 세션을 적용하면 session이 생성되는 시점에 데이터를 저장해준다.
- 세션을 쿠키로 반환할 때는 Base64로 인코딩하면 반환한다.