HttpSession and RESTful APIs
스프링 공식 문서
Spring Session은 Session을 헤더로 주고 받으면서 RESTful API와 동작할 수 있다.
Tomcat의 경우 SessionID를 JSESSIONID라는 키의 쿠키를 생성해서 set-cookie 헤더에 담아서 전달
Requset시 Header에 SessionId가 포함되어 절달된다면 (Cookie 헤더에 SeesionId가 전달될 것) 서블릿 컨테이너는 세션을 발급하지 않고 해당 SessionId에 해당하는 세션을 전달하게 되고, SessionId가 포함되지 않는다면 HttpSession을 요구하는 모든 요청에 대해 새로운 Session을 발급.
private String createSession(HttpServletRequest request, SignupRequestDto userDto) {
HttpSession session = request.getSession();
String sessionId = UUID.randomUUID().toString();
session.setAttribute("session_key", userDto);
return sessionId;
}
위 코드는 HttpSession을 활요해서 request에 sesisonId가 있는지 확인하고, 없으면 새로운 세션을 생성하고, 있다면 기존 세션을 사용하는 코드이다.
코드를 보면 JSESSION이라는 키로 쿠키를 세팅하지 않았는데 어떻게 JSESSION 키의 쿠키를 생성해서 set-cookie헤더로 전달되는 것일까?
HttpSession
HttpSession과 HttpServletRequest는 모두 인터페이스로 우리가 직접 커스텀할 수 있다.
public class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
public SessionRepositoryRequestWrapper(HttpServletRequest original) {
super(original);
}
public HttpSession getSession() {
return getSession(true);
}
public HttpSession getSession(boolean createNew) {
// create an HttpSession implementation from Spring Session
}
// ... other methods delegate to the original HttpServletRequest ...
}
getSession 메서드에서 HttpSession이 반환하는 세션을 커스텀할 수 있다.
JSESSION 생성원리
HttpServletRequest.getSession()
Spring에서 세션을 사용하기 위해 아래 코드를 사용함.
private String createSession(HttpServletRequest request, SignupRequestDto userDto) {
HttpSession session = request.getSession();
...
}
Spring에서 구현된 getSesison 메서드는 아래와 같다.
class ApplicationHttpRequest extends HttpServletRequestWrapper {
...
public HttpSession getSession() {
return this.getSession(true);
}
public HttpSession getSession(boolean create) {
if (this.crossContext) {
if (this.context == null) {
return null;
} else if (this.session != null && this.session.isValid()) {
return this.session.getSession();
} else {
HttpSession other = super.getSession(false);
if (create && other == null) {
other = super.getSession(true);
}
if (other != null) {
Session localSession = null;
try {
localSession = this.context.getManager().findSession(other.getId());
if (localSession != null && !localSession.isValid()) {
localSession = null;
}
} catch (IOException var5) {
}
if (localSession == null && create) {
localSession = this.context.getManager().createSession(other.getId());
}
if (localSession != null) {
localSession.access();
this.session = localSession;
return this.session.getSession();
}
}
return null;
}
} else {
return super.getSession(create);
}
}
}
14번째 라인에 보면 아래와 같은 코드가 있다
HttpSession other = super.getSession(false);
super가 가리키는 객체는 HttpServletRequestWrapper이므로 HttpServletRequestWrapper의 getSession을 살펴보자.
[+] getSession(), getSession(true), getSession(false)
getSession(), getSession(true)
- HttpSession이 존재하면 현재 HttpSession을 반환, 존재하지 않으면 새로운 세션을 생성
getSession(false)
- HttpSession이 존재하면 현재 HttpSession을 반환, 존재하지 않으면 새로 생성하지 않고 그냥 null 반환
ApplicationHttpRequest의 super.getSession()은 HttpServletRequest를 extends한 것이므로 HttpServletRequest의 geSession() 메서드 호출
JSESSION 생성 원리
HttpServletRequest.getSession()
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
...
private HttpServletRequest _getHttpServletRequest() {
return (HttpServletRequest)super.getRequest();
}
public HttpSession getSession(boolean create) {
return this._getHttpServletRequest().getSession(create);
}
public HttpSession getSession() {
return this._getHttpServletRequest().getSession();
}
}
getSession 메서드를 보면 `_getHttpServletRequest()`에서 super.getRequest() 에 `command + click` 하면 ServletRequestWrapper의 getRequest() 메서드로 이동한다.
ServletRequestWrapper.getSession()
public class ServletRequestWrapper implements ServletRequest {
private ServletRequest request;
...
public ServletRequest getRequest() {
return this.request;
}
}
ServletRequestWrapper의 get Request는 ServletRequest 를 반환한다.
HttpServletRequest.getSession() 은 ServletReqest의 getSession 메서드를 호출한다고 보면 된다.
Request.getSesison()
결국 Request의 getSession() 메서드가 호출된다.
servletReqeust -> HttpServletReqest -> Requset
public class Request implements HttpServletRequest {
...
public HttpSession getSession() {
return this.getSession(true);
}
public HttpSession getSession(boolean create) {
Session session = this.doGetSession(create);
return session == null ? null : session.getSession();
}
}
Request는 HttpServletReqeust의 구현체이고, getSession은 doGetSession으로 구현되어있다
doGetSession()
겁나길다...
protected Session doGetSession(boolean create) {
Context context = this.getContext();
if (context == null) {
return null;
} else {
if (this.session != null && !this.session.isValid()) {
this.session = null;
}
if (this.session != null) {
return this.session;
} else {
Manager manager = context.getManager();
if (manager == null) {
return null;
} else {
if (this.requestedSessionId != null) {
try {
this.session = manager.findSession(this.requestedSessionId);
} catch (IOException var14) {
IOException e = var14;
if (log.isDebugEnabled()) {
log.debug(sm.getString("request.session.failed", new Object[]{this.requestedSessionId, e.getMessage()}), e);
} else {
log.info(sm.getString("request.session.failed", new Object[]{this.requestedSessionId, e.getMessage()}));
}
this.session = null;
}
if (this.session != null && !this.session.isValid()) {
this.session = null;
}
if (this.session != null) {
this.session.access();
return this.session;
}
}
if (!create) {
return null;
} else {
boolean trackModesIncludesCookie = context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
if (trackModesIncludesCookie && this.response.getResponse().isCommitted()) {
throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
} else {
String sessionId = this.getRequestedSessionId();
if (!this.requestedSessionSSL) {
if ("/".equals(context.getSessionCookiePath()) && this.isRequestedSessionIdFromCookie()) {
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
Container[] var7 = this.getHost().findChildren();
int var8 = var7.length;
for(int var9 = 0; var9 < var8; ++var9) {
Container container = var7[var9];
Manager m = ((Context)container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException var13) {
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
sessionId = null;
}
}
this.session = manager.createSession(sessionId);
if (this.session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(context, this.session.getIdInternal(), this.isSecure());
this.response.addSessionCookieInternal(cookie);
}
if (this.session == null) {
return null;
} else {
this.session.access();
return this.session;
}
}
}
}
}
}
}
위 코드에서 세션을 만드는 코드를 보자면
this.session = manager.createSession(sessionId);
if (this.session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(context, this.session.getIdInternal(), this.isSecure());
this.response.addSessionCookieInternal(cookie);
}
manager의 createSession을 살펴보자.
Manager 인터페이스를 상속받은 ManagerBase 구현체에 구현되어 있다.
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
...
public Session createSession(String sessionId) {
if (this.maxActiveSessions >= 0 && this.getActiveSessions() >= this.maxActiveSessions) {
++this.rejectedSessions;
throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"), this.maxActiveSessions);
} else {
Session session = this.createEmptySession();
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(this.getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = this.generateSessionId();
}
session.setId(id);
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized(this.sessionCreationTiming) {
this.sessionCreationTiming.add(timing);
this.sessionCreationTiming.poll();
return session;
}
}
}
}
protected String generateSessionId() {
return this.sessionIdGenerator.generateSessionId();
}
여기서 JSESSIONID를 만드는 방법을 볼 수 있다.
StandardSessionIdGenerator의 generatorSessionId()
SessionIdGenerator -> SessionIdGeneratorBase 구현체 -> StandardSessionIdGenerator
public class StandardSessionIdGenerator extends SessionIdGeneratorBase {
...
public String generateSessionId(String route) {
byte[] random = new byte[16];
int sessionIdLength = this.getSessionIdLength();
StringBuilder buffer = new StringBuilder(2 * sessionIdLength + 20);
int resultLenBytes = 0;
while(resultLenBytes < sessionIdLength) {
this.getRandomBytes(random);
for(int j = 0; j < random.length && resultLenBytes < sessionIdLength; ++j) {
byte b1 = (byte)((random[j] & 240) >> 4);
byte b2 = (byte)(random[j] & 15);
if (b1 < 10) {
buffer.append((char)(48 + b1));
} else {
buffer.append((char)(65 + (b1 - 10)));
}
if (b2 < 10) {
buffer.append((char)(48 + b2));
} else {
buffer.append((char)(65 + (b2 - 10)));
}
++resultLenBytes;
}
}
if (route != null && route.length() > 0) {
buffer.append('.').append(route);
} else {
String jvmRoute = this.getJvmRoute();
if (jvmRoute != null && jvmRoute.length() > 0) {
buffer.append('.').append(jvmRoute);
}
}
return buffer.toString();
}
}
{
JSESSIONID를 생성하는 방법은 아래와 같다.
16Byte 의 랜덤값(SHA1PRNGE 또는 각 플랫폼의 기본 난수 생성기) 을 16진수의 String 으로 변환하여 route로 받는 값이 있는 경우 뒤에 `,route`를 추가하게 되고, 그렇지 않을 경우 `.jvmRoute`을 추가하게 된다.
여기서 Route나 jvmRoute는 서블릿 컨테이너에 접속한 사용자를 구분하는 값이 되는데 이를 구현한 코드는 위 코드에서 (StandardSessionIdGenerator의 generatorSessionId 메서드) 확인 가능
기존 JSESSION 은 어떻게 확인하는가
Request의 doGetSession 메서드에서 m.findSession부분을 보면
for(int var9 = 0; var9 < var8; ++var9) {
Container container = var7[var9];
Manager m = ((Context)container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException var13) {
}
}
}
ManagerBase의 findSession()으로 가서 확인해보면
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
protected Map<String, Session> sessions;
public Session findSession(String id) throws IOException {
return id == null ? null : (Session)this.sessions.get(id);
}
}
session이 Map으로 관리되고 있고, JSESSIONID가 들어오면 그에 맞는 Session을 반환하도록 되어있다.
JSESSION이 쿠키로 설정되는 과정
private String createSession(HttpServletRequest request, SignupRequestDto userDto) {
HttpSession session = request.getSession();
}
`request.getSession()`이 호출되면 Request의 getSession이 호출되고, 결과적으로 doGetSession 메서드가 실행된다.
session이 없고 새로 생성될 때 cookie에도 JSESSION의 key로 쿠키에 생성이되는데
쿠키가 세팅되는 과정은 다음과 같다.
Requset의 doGetSession 메서드의 코드 중 일부이다. (쿠키를 설정하는 코드)
this.session = manager.createSession(sessionId);
if (this.session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(context, this.session.getIdInternal(), this.isSecure());
this.response.addSessionCookieInternal(cookie);
}
ApplicationSessionCookieConfig.createSessionCookie메서드에서 쿠키가 생성되고 response에 쿠키를 담아서 클라이언트에게 보내주는 과정을 거치게 된다.
ApplicationSessionCookieConfig의 createSesisonCookie 메서드
public class ApplicationSessionCookieConfig implements SessionCookieConfig {
...
public static Cookie createSessionCookie(Context context, String sessionId, boolean secure) {
SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig();
Cookie cookie = new Cookie(SessionConfig.getSessionCookieName(context), sessionId);
cookie.setMaxAge(scc.getMaxAge());
if (context.getSessionCookieDomain() == null) {
if (scc.getDomain() != null) {
cookie.setDomain(scc.getDomain());
}
} else {
cookie.setDomain(context.getSessionCookieDomain());
}
if (scc.isSecure() || secure) {
cookie.setSecure(true);
}
if (scc.isHttpOnly() || context.getUseHttpOnly()) {
cookie.setHttpOnly(true);
}
cookie.setAttribute("Partitioned", Boolean.toString(context.getUsePartitioned()));
cookie.setPath(SessionConfig.getSessionCookiePath(context));
Iterator var5 = scc.getAttributes().entrySet().iterator();
while(var5.hasNext()) {
Map.Entry<String, String> attribute = (Map.Entry)var5.next();
switch ((String)attribute.getKey()) {
case "Comment":
case "Domain":
case "Max-Age":
case "Path":
case "Secure":
case "HttpOnly":
break;
default:
cookie.setAttribute((String)attribute.getKey(), (String)attribute.getValue());
}
}
return cookie;
}
}
위 코드에서 쿠키를 생성하는 코드만 보면 아래와 같다.
Cookie cookie = new Cookie(SessionConfig.getSessionCookieName(context), sessionId);
SessionConfig.getSesisonCookieName()
SesisonConfig.getSesisonCookieName에서 JSESSIONID 를 반환한다. 코드는 아래와 같다.
public class SessionConfig {
public static String getSessionCookieName(Context context) {
return getConfiguredSessionCookieName(context, "JSESSIONID");
}
private static String getConfiguredSessionCookieName(Context context, String defaultName) {
if (context != null) {
String cookieName = context.getSessionCookieName();
if (cookieName != null && cookieName.length() > 0) {
return cookieName;
}
SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig();
cookieName = scc.getName();
if (cookieName != null && cookieName.length() > 0) {
return cookieName;
}
}
return defaultName;
}
}
그림으로 보는 HttpSession 동작 과정
ref
'Spring > Spring MVC' 카테고리의 다른 글
[Spring] Spring Session (1) | 2024.10.26 |
---|---|
[Spring MVC] HandlerAdapter 가 필요한 이유 - 유연한 컨트롤러 (0) | 2024.10.08 |
[웹 애플리케이션 이해] 동시 요청 - 멀티 쓰레드 (2) | 2024.10.07 |
[웹 애플리케이셔 이해] 서블릿, 서블릿 컨테이너 (0) | 2024.10.07 |
[Spring Boot] WAS를 사용하면 WS는 필요없다? (0) | 2024.10.06 |