์ด์ ๊ธ์ ์ด์ด์ ๋ฐ๋ก ์ธ์ ์ ์ด์ฉํ ๋ก๊ทธ์ธ๋ ๊ตฌํํด ๋ณด๊ฒ ์ต๋๋ค! (ํ์ ์๋ณ์ JWT๊น์ง ํ๊ณ ๋ ๋ค์ ๊ฐ๊ฐ ์์ฑํ๊ฒ ์ต๋๋ค.)
์ธ์
์ธ์ ์ ์ฟ ํค๋ง์ ์ฌ์ฉํ๋ ๋ฐฉ์๊ณผ๋ ์กฐ๊ธ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ด๋ฃจ์ด์ง๋๋ค.
์ธ์ ์ ์ฌ์ฉ ์ด์
์ธ์ ์ ์ ์ฐ๋์ง์ ๋ํด์๋ ์ฟ ํค์ ๋ํด ๋ค์ ์์๋ด์ผ ํฉ๋๋ค.
๊ธฐ์กด ์ฟ ํค ๋ฐฉ์์, ์ฟ ํค ์์ ์๋ฒ๊ฐ ๋ธ๋ผ์ฐ์ ๋ฅผ ์๋ณํ ์ ์๋ ๋ชจ๋ ์ ๋ณด๋ค์ ๋ค ๊ธฐ๋กํด๋์ผ ํ์ต๋๋ค.
(๋๋ค์.๋น๋ฐ๋ฒํธ๋ฅผ ์ฟ ํค๋ก ๊ทธ๋๋ก ๋ฃ์ด๋ ๊ฒ ๋ฑ)
ํ์ง๋ง ์ด๋ ๊ฒ ๋๋ค๋ฉด ์ฟ ํค๋ฅผ ๋ง์ฝ ํ์ทจ๋นํ์ ๊ฒฝ์ฐ (๊ฐ๋๊ฐ ์ฌํ๊ฐ์ ์ ๋ถ์ฆ์ ํ์น๋ ๋ฑ),
๋ธ๋ผ์ฐ์ (์ฌํ๊ฐ)์ ๋ํ ๋ชจ๋ ์ ๋ณด๋ฅผ ๋ค ํ์
ํ ์ ์๊ฒ ์ฃ ? ๋ฐ๋ก ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ๊ทน๋ณตํ๊ธฐ ์ํด ์ธ์
์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ธ์ ์ ํน์ง
์๋ฒ๋ ์ ์ ๊ฐ ๋ก๊ทธ์ธ์ ํ๋ฉด ์ธ์ DB์ ์ ์ ๋ฅผ ์์ฑํฉ๋๋ค.
์ธ์ DB์๋ ๋ณ๋์ ID๊ฐ ์๊ณ , ์ด ID๋ ์ฟ ํค๋ฅผ ํตํด ๋ธ๋ผ์ฐ์ ๋ก ๋์์ ์ ์ฅ๋ฉ๋๋ค.
์ดํ ์ ์ ๊ฐ ์๋ก์ด ํ์ด์ง์ ์ ๊ทผํ ๋, ์๋ฒ๋ ์ธ์ ID๊ฐ ๋ด๊ธด ์ฟ ํค์ ์ธ์ ID๋ฅผ ํ์ธํ์ฌ ์๋ณํฉ๋๋ค.
์ด ๋ฐฉ์์ ๊ธฐ์กด์ ์ฟ ํค๋ง์ ์ผ์ ๋์ ๋ฌ๋ฆฌ ์ธ์ ID์ ๋ํ ๊ฐ๋ง ์ฟ ํค์ ๋ด๊ธฐ๊ธฐ ๋๋ฌธ์, ์ด์ ์ฒ๋ผ ํ์์ ๋ํ ๋ฏผ๊ฐํ ์ ๋ณด๋ค์ด ๊ธฐ๋ก๋์ง ์์ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค. ๋ํ, ์ธ์ ๋ฐฉ์์ ์ด์ฉํ๋ฉด ํน์ ์ ์ ๋ฅผ ์ซ์๋ด๊ณ ์ถ์ ๋ ์ธ์ ์ ์ญ์ ํ๋ฉด ๋ฉ๋๋ค. ์ด๋ ๋ทํ๋ฆญ์ค, ์ธ์คํ๊ทธ๋จ ๋ฑ์์ "ํ์ฌ ๋ก๊ทธ์ธ ํ ๊ธฐ๊ธฐ"๋ฅผ ๋ก๊ทธ์์ ์ํฌ ๋ ๋ฑ์์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
ํ์ง๋ง ๋ก๊ทธ์ธ์ ํ ์ ์ ๋ค์ ์ธ์ ์ ๋ชจ๋ ๊ด๋ฆฌํด์ผ ํ๋ค๋ ์ (Redis๋ฅผ ์ฌ์ฉํ๊ธฐ๋ ํฉ๋๋ค.), ์ธ์ ํ์ด์ฌํน (Session Hi-jacking)์ด ์ผ์ด๋ ์ ์๋ค๋ ์ ๋ฑ ์ธ์ ๋ฐฉ์ ๋ํ ๋ฌธ์ ์ ์ด ์กด์ฌํฉ๋๋ค.
์๋ฒ๋ Stateless ๊ตฌ์กฐ์ฌ์ผ ํ๋๋ฐ, ๊ทธ๋ฌ๋ฉด ์ธ์ ๋ฐฉ์์ ์ด ๊ตฌ์กฐ๊ฐ ์๋ ๊ฑด๊ฐ์?
๋ง์ต๋๋ค. ์ฟ ํค, ์ธ์ ๋ฐฉ์์ ๊ธฐ์กด HTTP์ ํน์ง์ธ Stateless์ธ ์ํฉ์์ ๋งค๋ฒ ์ฌ์ฉ์ ์ธ์ฆ์ด ์ด๋ค์ง๋ค๋ ๋จ์ ์ ๋ณด์ํ๊ธฐ ์ํด ๋์จ ๊ฐ๋ ์ ๋๋ค. ์ธ์ ๋ฐฉ์์ ๊ฒฝ์ฐ, ๋ด๋ถ ์ฝ๋๋ฅผ ๋ฏ์ด๋ณด๋ฉด ์๋ฒ๋ ๋ณ๋์ ์ธ์ DB๋ก๋ถํฐ ์ธ์ ID๋ฅผ ํ์ธํ๋ ๊ตฌ์กฐ์์ ์ ์ ์์ต๋๋ค. ์ด๋ ๋ง์น ์ฟ ํค ๋ฐฉ์์์์ ์์๋ก ๋ค๋ฉด ๊ฒฝ์ฐฐ (์๋ฒ)์ด ์ฌํ๊ฐ (๋ธ๋ผ์ฐ์ )์ ์ ๋ถ (์ธ์ ID)์ ํ์ธํ ๋, ์ ํจ์ฑ์ ์ธ์ฆํด ์ค ๋์ฌ๊ด (๋๋ ์ถ์ ๊ตญ ์ฌ๋ฌด์๊ฐ ๋๊ฒ ์ฃ ?) (์ธ์ DB ์ ์ฅ์ - Stateful)์ ๋ฌผ์ด๋ณด๋ ๊ฒ๊ณผ ๊ฐ์ต๋๋ค.
๊ทธ๋ฌ๋ฉด ์คํ๋ง ์ฝ๋๋ฅผ ์ข ๋ ์์ธํ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
HttpServletRequest
์ฟ ํค ๊ธ์์ HttpServletRequest์ ๋ํด์๋ ์คํ๋ง ์๋ธ๋ฆฟ ํ๊ฒฝ์์ ์ด๋ฃจ์ด์ง ์ฌ์ฉ์์ ์์ฒญ์ด ๋ด๊ธด ์์ฒญ ์ ๋ณด๋ผ๊ณ ํ์์ต๋๋ค. ์ธ์ ์ ์ด HttpServletRequest๋ก๋ถํฐ ๋ฐ์ ์ ์์ต๋๋ค.
@PostMapping("/login/session")
public ResponseEntity<Void> loginWithSession(@RequestBody @Valid final LoginRequest request,
final HttpServletRequest httpRequest) {
Member loginMember = authService.nonJwtLogin(request);
HttpSession session = httpRequest.getSession(); // HttpSession
saveSessionProperties(session, loginMember);
return ResponseEntity.ok()
.build();
}
getSession์ ๋ณด๋ฉด, HttpSession์ ๊ฐ์ ธ์ต๋๋ค. ๊ทธ๋ฐ๋ฐ HttpServletRequest๋ ์ธํฐํ์ด์ค์ด๊ธฐ ๋๋ฌธ์, ์ค์ ์ ์ผ๋ก getSession์ ์ํํ ๊ตฌํ์ฒด๋ฅผ ๋ด์ผ ํฉ๋๋ค.
HttpServletRequest์ ๊ตฌํ์ฒด๋ฅผ ๋ณด๋ฉด ์ฌ๋ฌ ๊ตฌํ์ฒด๋ค์ด ์๋๋ฐ, ์ด ์ค RequestFacade๋ฅผ ํ๋๋ค.
RequestFacade
RequestFacade๋ ์๋์ ๊ฐ์ด ๊ตฌํ๋์ด ์์ต๋๋ค.
public class RequestFacade implements HttpServletRequest {
...
/**
* The wrapped request.
*/
protected Request request = null;
/**
* Construct a wrapper for the specified request.
*
* @param request The request to be wrapped
*/
public RequestFacade(Request request) {
this.request = request;
}
...
@Override
public HttpSession getSession(boolean create) {
checkFacade();
if (SecurityUtil.isPackageProtectionEnabled()) {
return AccessController.doPrivileged(new GetSessionPrivilegedAction(create));
} else {
return request.getSession(create);
}
}
@Override
public HttpSession getSession() {
return getSession(true);
}
...
private void checkFacade() {
if (request == null) {
throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
}
}
}
- getSession()์ ํ ๊ฒฝ์ฐ ๊ธฐ๋ณธ์ ์ผ๋ก create ๊ฐ์ด true๋ก ๋์ด ์ ๋ฌ๋ฉ๋๋ค.
- checkFacade๊ฐ ์คํ๋ฉ๋๋ค. request (Request: HttpServletRequest๋ฅผ ๊ตฌํํ ๋ ๋ค๋ฅธ ๊ตฌํ์ฒด)๊ฐ null์ด๋ผ๋ฉด ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค. ์ด Request๋ ์๋ ํฐ์บฃ์์ ์คํ๋๋ ์์ฒญ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ผ๋ฉฐ, Request ์์ ์๋ public ๋ฉ์๋๋ค ์ค ๊ฐ๋ฐ์๊ฐ ์ธ๋ถ์์ ๋ค์ด์บ์คํ ํ๋ฉด ์ ๋๋ ๋ฉ์๋๋ค์ด ์๊ธฐ์ ๊ฐ๋ฐ์์๊ฒ ์ ๊ณตํ๋ ํด๋์ค๊ฐ ๋ฐ๋ก RequestFacade์ ๋๋ค. ๋ฐ๋๋ก Response๋ฅผ ๋ณดํธํ๋ ResponseFacade๋ ์์ต๋๋ค.
- SecurityUtil์ ๋ณด์๊ณผ ๊ด๋ จ๋ ์ ํธ๋ฆฌํฐ์
๋๋ค. ํจํค์ง ๋ณด์ ๋ฉ์ปค๋์ฆ์ด ํ์ฑํ๋์ด ์์ ๋ AccessController๋ก๋ถํฐ doPrivileged๋ฅผ ์คํํฉ๋๋ค. (SecurityUtil์ SecurityManager๋ฅผ ์ฌ์ฉํ๋ฉฐ, AccessControlContext์ ์ฐ๊ด๋ฉ๋๋ค. ์ด ๋ถ๋ถ์ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ฌ์ฉํ๋ ํ๋ฆ๊ณผ ๋น์ทํ ๊ฒ ๊ฐ์ผ๋, ๊ฐ์ธ์ ์ธ ์ถ์ธก์
๋๋ค.)
- ๋๋ฒ๊น ์ ํด ๋ณด๋ฉด, else๋ฌธ์ผ๋ก ๋์ด๊ฐ ๋ค request.getSession(true)๊ฐ ๋์์ต๋๋ค.
request.getSession(true)์ ๋ณด๊ฒ ์ต๋๋ค.
Request
public class Request implements HttpServletRequest {
...
/**
* The currently active session for this request.
*/
protected Session session = null;
/**
* @return the session associated with this Request, creating one if necessary and requested.
*
* @param create Create a new session if one does not exist
*/
@Override
public HttpSession getSession(boolean create) {
Session session = doGetSession(create);
if (session == null) {
return null;
}
return session.getSession();
}
...
protected Session doGetSession(boolean create) {
// There cannot be a session if no context has been assigned yet
Context context = getContext();
if (context == null) {
return null;
}
// Return the current session if it exists and is valid
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
return session;
}
// Return the requested session if it exists and is valid
Manager manager = context.getManager();
if (manager == null) {
return null; // Sessions are not supported
}
if (requestedSessionId != null) {
try {
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
} else {
log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
}
session = null;
}
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
session.access();
return session;
}
}
// Create a new session if requested and the response is not committed
if (!create) {
return null;
}
boolean trackModesIncludesCookie =
context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
}
// Re-use session IDs provided by the client in very limited
// circumstances.
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
// If the session ID has been obtained from the SSL handshake then
// use it.
} else if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie())) {
/*
* This is the common(ish) use case: using the same session ID with multiple web applications on the same
* host. Typically this is used by Portlet implementations. It only works if sessions are tracked via
* cookies. The cookie must have a path of "/" else it won't be provided for requests to all web
* applications.
*
* Any session ID provided by the client should be for a session that already exists somewhere on the host.
* Check if the context is configured for this to be confirmed.
*/
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
for (Container container : getHost().findChildren()) {
Manager m = ((Context) container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException e) {
// Ignore. Problems with this manager will be
// handled elsewhere.
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
sessionId = null;
}
// Manager๋ก๋ถํฐ ์ธ์
์ ๋ง๋ญ๋๋ค. ์ด ๋ถ๋ถ์ด ์ค์ํฉ๋๋ค.
session = manager.createSession(sessionId);
// ์ธ์
์ด ๋ง๋ค์ด์ก๊ณ , ์ฟ ํค ๋ฐฉ์์ ์ฌ์ฉํ ์ ์์ผ๋ฉด (์ฌ์ฉํ ์ ์์ต๋๋ค.) ์ฟ ํค๋ฅผ ๋ง๋ค๊ณ Response์ ๋ฃ์ต๋๋ค.
// Creating a new session cookie based on that session
if (session != null && trackModesIncludesCookie) {
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
session.access();
return session;
}
}
- ์ธ์ ์ ์๋ฏธํ๋ Session์ ์ฒ์์ null๋ก ์ค์ ๋์ด ์์ต๋๋ค.
- doGetSession(true)๋ก ์ธ์
ํ๋์ ์๋ํฉ๋๋ค.
- ํ๋จ๋ถ๋ฅผ ๋ณด๋ฉด, ์ธ์ ์ด manager๋ก๋ถํฐ createSession์ ํตํด ๋ง๋ค์ด์ง์ ํ์ธํ ์ ์์ต๋๋ค.
- ๊ทธ๋ฆฌ๊ณ ApplicationSessionCookieConfig๋ฅผ ํตํด ์ฟ ํค๋ฅผ ๋ง๋ญ๋๋ค. ๊ทธ๋ค์ Response์ ์ฟ ํค๋ฅผ ๋ฃ์ต๋๋ค.
Context
ํฐ์บฃ์์์ Context๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ํ๊ณ ๊ด๋ฆฌํ๋ ๊ฐ๋ ์ ๋๋ค. ๊ตฌํ์ฒด๋ก๋ FailedContext, StandardContext, TomcatEmbeddedContext๊ฐ ์์ต๋๋ค.
- FailedContext: ์ ํ๋ฆฌ์ผ์ด์ ์ ์ค์ ์ค๋ฅ, ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถฉ๋, ์ฝ๋ ๋ฌธ์ ์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ํฐ์บฃ์ ํด๋น ์ ํ๋ฆฌ์ผ์ด์ ์ FailedContext๋ก ํ์ํ๊ณ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋ฉ๋๋ค.
- StandardContext: ํฐ์บฃ์ ํ์ค ์น ์ ํ๋ฆฌ์ผ์ด์
์ปจํ
์คํธ์ด๋ฉฐ, WAR ํ์ผ ๋๋ ๊ฐ๋ณ์ ์ผ๋ก ๋ฐฐํฌ๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ์คํํ ๋ ์ฌ์ฉ๋ฉ๋๋ค.
- StandardContext ์์๋ ApplicationContext๊ฐ ์์ต๋๋ค. ์ด ApplicationContext๋ ์คํ๋ง์ ApplicationContext์ ๋ค๋ฆ ๋๋ค. ๋ํ์ ์ผ๋ก ์ธ์ ์ ์ฟ ํค ๋ฐฉ์๊ณผ URL ๋ฐฉ์์ ์ธ ์ ์๋๋ฐ, ์ด๋ฌํ ๋ด์ฉ์ ์๋ฏธํ๋ defaultSessionTrackingModes๊ฐ COOKIE, URL๋ก ๋ด๊ฒจ ์์ต๋๋ค. (Enum์ผ๋ก๋ COOKIE, URL, SSL๋ก ๊ตฌ์ฑ๋์ด ์์ต๋๋ค. SSL์ ์ฌ์ฉํ ์ ์๋ ํ๊ฒฝ์ด๋ผ๋ฉด SSL์ ์ฌ์ฉํฉ๋๋ค.)
- ๋ํ StandardContext๋ก๋ถํฐ Manager๋ฅผ ์ป์ ์ ์์ต๋๋ค.
- TomcatEmbeddedContext: ํฐ์บฃ์ ๋ด์ฅ (embedded) ํ๊ฒฝ์์ ์ฌ์ฉ๋๋ ์ปจํ ์คํธ ๊ตฌํ์ฒด์ ๋๋ค. ์ฃผ๋ก ์คํ๋ง ๋ถํธ์ ๊ฐ์ด ๋ด์ฅ๋ ํฐ์บฃ ํ๊ฒฝ์์ ์ฌ์ฉ๋ฉ๋๋ค. StandardContext๋ก๋ถํฐ ์์๋ฐ์ต๋๋ค.
Manager
Manager๋ ์ธ์ DB์์ ์ฐ๊ฒฐ์ ๊ด๋ฆฌํด ์ฃผ๋ ์ญํ ์ ํฉ๋๋ค. StandardContext๋ก๋ถํฐ getManager๋ฅผ ํธ์ถํ์ฌ Manager๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
์ด๋, StandardContext์ startInternal์ ํตํด ์ด๊ธฐํ๋ฉ๋๋ค. getManager๋ฅผ ํธ์ถํ ๋๋ ์ฝ๊ธฐ ๋ฝ, setManager๋ฅผ ํธ์ถํ ๋๋ ์ฐ๊ธฐ ๋ฝ์ด ์ฌ์ฉ๋จ์ ๋ณผ ์ ์์ต๋๋ค.
...
@Override
public Manager getManager() {
Lock readLock = managerLock.readLock(); // ์ฝ๊ธฐ ๋ฝ (read lock)์ด ๊ด์ฐฐ๋ฉ๋๋ค.
readLock.lock();
try {
return manager;
} finally {
readLock.unlock();
}
}
@Override
protected synchronized void startInternal() throws LifecycleException {
...
// Acquire clustered manager
Manager contextManager = null;
Manager manager = getManager();
if (manager == null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.cluster.noManager",
Boolean.valueOf((getCluster() != null)), Boolean.valueOf(distributable)));
}
if ((getCluster() != null) && distributable) {
try {
contextManager = getCluster().createManager(getName());
} catch (Exception ex) {
log.error(sm.getString("standardContext.cluster.managerError"), ex);
ok = false;
}
} else {
contextManager = new StandardManager();
}
}
// Configure default manager if none was specified
if (contextManager != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.manager", contextManager.getClass().getName()));
}
setManager(contextManager);
}
...
}
...
@Override
public void setManager(Manager manager) {
Lock writeLock = managerLock.writeLock(); // ์ฐ๊ธฐ ๋ฝ (write lock)์ด ๊ด์ฐฐ๋ฉ๋๋ค.
writeLock.lock();
Manager oldManager = null;
try {
// Change components if necessary
oldManager = this.manager;
if (oldManager == manager) {
return;
}
this.manager = manager;
// Stop the old component if necessary
if (oldManager instanceof Lifecycle) {
try {
((Lifecycle) oldManager).stop();
((Lifecycle) oldManager).destroy();
} catch (LifecycleException e) {
log.error(sm.getString("standardContext.setManager.stop"), e);
}
}
// Start the new component if necessary
if (manager != null) {
manager.setContext(this);
}
if (getState().isAvailable() && manager instanceof Lifecycle) {
try {
((Lifecycle) manager).start();
} catch (LifecycleException e) {
log.error(sm.getString("standardContext.setManager.start"), e);
}
}
} finally {
writeLock.unlock();
}
// Report this property change to interested listeners
support.firePropertyChange("manager", oldManager, manager);
}
ManagerBase
ManagerBase๋ Manager ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ ๊ตฌํ์ฒด์ ๋๋ค. ์ธ์ ID๋ฅผ ์์ฑํด ์ฃผ๋ SessionIdGenerator, ํค ์์ฑ ์๊ณ ๋ฆฌ์ฆ์ธ secureRandomAlgorithm ๋ฑ์ด ์์ต๋๋ค.
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
...
/**
* The Context with which this Manager is associated.
*/
private Context context;
/**
The name of the algorithm to use to create instances of java.security.SecureRandom
which are used to generate session IDs.
If no algorithm is specified, SHA1PRNG is used.
If SHA1PRNG is not available, the platform default will be used.
To use the platform default (which may be SHA1PRNG), specify the empty string.
If an invalid algorithm and/or provider is specified the SecureRandom instances will be created using the defaults.
If that fails, the SecureRandom instances will be created using platform defaults.
*/
protected String secureRandomAlgorithm = SessionIdGeneratorBase.DEFAULT_SECURE_RANDOM_ALGORITHM;
protected SessionIdGenerator sessionIdGenerator = null;
/**
* The set of currently active Sessions for this Manager, keyed by session identifier.
*/
protected Map<String,Session> sessions = new ConcurrentHashMap<>();
...
}
TomcatEmbeddedContext์์๋ setManager๋ฅผ ์ฌ์ ์ํ์ฌ ๋ค์๊ณผ ๊ฐ์ด SessionIdGenerator๋ฅผ LazySessionIdGenerator๋ก ํ ๋นํด ์ค๋๋ค. (๊ทธ๋ค์์ StandardContext์ setManager๊ฐ ์ด์ด์ ํธ์ถ๋ฉ๋๋ค.)
...
@Override
public void setManager(Manager manager) {
if (manager instanceof ManagerBase) {
manager.setSessionIdGenerator(new LazySessionIdGenerator());
}
super.setManager(manager);
}
...
}
์ดํ ManagerBase์ startInternal์์ sessionIdGenerator.start()๊ฐ ํธ์ถ๋ฉ๋๋ค.
@Override
protected void startInternal() throws LifecycleException {
// Ensure caches for timing stats are the right size by filling with
// nulls.
synchronized (sessionCreationTiming) {
while (sessionCreationTiming.size() < TIMING_STATS_CACHE_SIZE) {
sessionCreationTiming.add(null);
}
}
synchronized (sessionExpirationTiming) {
while (sessionExpirationTiming.size() < TIMING_STATS_CACHE_SIZE) {
sessionExpirationTiming.add(null);
}
}
/* Create sessionIdGenerator if not explicitly configured */
// SessionIdGenerator๊ฐ LazySessionIdGenerator์ด๋ฏ๋ก ์ฌ๊ธฐ๋ ๊ฑด๋๋๋๋ค.
SessionIdGenerator sessionIdGenerator = getSessionIdGenerator();
if (sessionIdGenerator == null) {
sessionIdGenerator = new StandardSessionIdGenerator();
setSessionIdGenerator(sessionIdGenerator);
}
// ์ฌ๊ธฐ๊ฐ ์คํ๋ฉ๋๋ค.
sessionIdGenerator.setJvmRoute(getJvmRoute());
if (sessionIdGenerator instanceof SessionIdGeneratorBase) {
SessionIdGeneratorBase sig = (SessionIdGeneratorBase) sessionIdGenerator;
sig.setSecureRandomAlgorithm(getSecureRandomAlgorithm());
sig.setSecureRandomClass(getSecureRandomClass());
sig.setSecureRandomProvider(getSecureRandomProvider());
}
if (sessionIdGenerator instanceof Lifecycle) {
((Lifecycle) sessionIdGenerator).start();
} else {
// Force initialization of the random number generator
if (log.isDebugEnabled()) {
log.debug("Force random number initialization starting");
}
sessionIdGenerator.generateSessionId();
if (log.isDebugEnabled()) {
log.debug("Force random number initialization completed");
}
}
}
ํค ์์ฑ ๋ฐฉ์์ด ๊ถ๊ธํฉ๋๋ค!
์ด๋ฒ์๋ ์์ฑ๋๋ ์ธ์ ID์ ๋ํด ๋ณด๊ฒ ์ต๋๋ค.
SessionConfig๋ฅผ ๋ณด๋ฉด, ๊ธฐ๋ณธ ์ฟ ํค ์ด๋ฆ์ผ๋ก JSESSIONID, URL ํ๋ผ๋ฏธํฐ ์ด๋ฆ์ผ๋ก jsessionid๊ฐ ์ฐ์์ ์ ์ ์์ต๋๋ค.
SessionIdGenerator์ ์ถ์ ํด๋์ค๋ก๋ SessionIdGeneratorBase๊ฐ ์๊ณ , SessionIdGeneratorBase์ ์์ ํด๋์ค๋ก StandardSessionIdGenerator๊ฐ ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฐ๋ก ์ฌ๊ธฐ์์ ์ธ์ ID ์์ฑ ๋ฐฉ์์ด ๋์ ์์ต๋๋ค. (LazySessionIdGenerator๋ StandardSessionIdGenerator์ ์์ ํด๋์ค)
- 16 ๋ฐ์ดํธ ๋ฐฐ์ด์ ํฌ๊ธฐ์ ๋๋ค.
- sessionIdLength๋ 16์ ๋๋ค.
- ์ฃผ์์ ๋ณด๋ฉด, 16์ง์ ๋ฌธ์์ด์ ๋ฐํํจ์ ์ ์ ์์ต๋๋ค.
์ธ์ DB, ์ฆ "์ ์ฅ์" ๊ฐ์ ๊ฑด ์ธ์ ๋์ค๋์?
์ง๊ธ๊น์ง์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋ญ๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ค๋ฌ์ด ์ฝ๋๋ ๋์ค์ง ์์์ต๋๋ค. ์ธ์ ์ด๋ผ๋ ๊ฒ๋ง ๋์์ง, ์ค์ ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋๋๋ ๊ฒ ๋ณด์ด์ง ์์๋๋ฐ, ๊ทธ ๋ต์ Store์์ ์ฐพ์ ์ ์์ต๋๋ค.
Store
์ญ์๋ ์ฝ๋๋ฅผ ๋ณด๊ฒ ์ต๋๋ค.
/**
* A <b>Store</b> is the abstraction of a Catalina component that provides
* persistent storage and loading of Sessions and their associated user data.
* Implementations are free to save and load the Sessions to any media they
* wish, but it is assumed that saved Sessions are persistent across
* server or context restarts.
*
* @author Craig R. McClanahan
*/
public interface Store {
Manager getManager();
void setManager(Manager manager);
int getSize() throws IOException;
void addPropertyChangeListener(PropertyChangeListener listener);
String[] keys() throws IOException;
Session load(String id)
throws ClassNotFoundException, IOException;
void remove(String id) throws IOException;
void clear() throws IOException;
void removePropertyChangeListener(PropertyChangeListener listener);
void save(Session session) throws IOException;
}
์ด์ ๊ฐ์ด ์ค์๊ฒ ๋์? ์ ํฌ๊ฐ ์์ฃผ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฆฌํฌ์งํฐ๋ฆฌ์ ๋น์ทํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ์ฐ๊ฒฐ์ ๊ด๋ฆฌํด ์ฃผ๋ Manager์ ๋ํ getter/setter, ์ ์ฅ๋ ์ธ์ ์๋ฅผ ์ถ๋ ฅํ getSize, ์ธ์ ID๋ฅผ ํตํด ์ธ์ ์ ์กฐํํ๋ load, ์ธ์ ์ ์ญ์ ํ๋ remove, ์ธ์ ์ ์ ๋ถ ์์ ๋ clear, ์ธ์ ์ ์ ์ฅํ๋ save ๋ฉ์๋ ๋ฑ์ด ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด Store ์ธํฐํ์ด์ค๋ DataSourceStore, FileStore, StoreBase ๊ตฌํ์ฒด๊ฐ ์์ต๋๋ค.
DataSourceStore
DataSourceStore์ load ๋ฉ์๋๋ฅผ ๋ณด๋ฉด ๋ ์ ์์ค ์ ์์ต๋๋ค. ์ธ์ ์ ์กฐํํ๊ธฐ ์ํ SQL์ด ์๊ณ , ์ฐ๊ฒฐ์ ํ๊ธฐ ์ํด Connection์ ๊ฐ์ ธ์ค๋ ค๊ณ getConnection์ ํธ์ถํฉ๋๋ค. (์ ํฌ๊ฐ JDBCTemplate์ ์จ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ํ ๋์ ๋น์ทํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋๋ค.)
๊ทธ๋ผ getConnection์ ์ด๋ป๊ฒ ๋์ด ์๋์ง๋ฅผ ๋ณด๊ฒ ์ต๋๋ค.
open ๋ฉ์๋๋ก ๊ฐ์ ธ์ค๋๊ตฐ์! ๋ ๋ค์ด๊ฐ ๋ด ์๋ค.
๋๋์ด ํด๋ต์ด ๋์์ต๋๋ค. "java:comp/env"๋ฅผ ์ด์ฉํด์ ๋ฐ์ดํฐ์์ค๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋๋ฐ์, ์ด๊ฒ์ JNDI (Java Naming and Directory Interface)๋ฅผ ์ด์ฉํ์ฌ ํฐ์บฃ์ ์๋ XML์ ์์๋ฅผ ์ฝ์ด ๋ค์ด๋ ๊ฒ์ ๋๋ค.
๋ค๋ง ๋ ์์๋ด์ง ๋ชปํ ์ ์ ํฐ์บฃ์ ์ง์ ์ค์นํ์ ๋์๋ server.xml์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ จ ์ ๋ณด๋ฅผ ์์ฑํจ์ผ๋ก์จ ์ค์ ํ ์ ์๋ค๊ณ ํ๋๋ฐ, ์คํ๋ง์ ๊ฒฝ์ฐ์๋ ๋ด์ฅ๋์ด ์๋ค ๋ณด๋ server.xml์ด ๋ณด์ด์ง ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
ํฐ์บฃ์ ๋ด๋ถ์ ์ผ๋ก ๋ฏ์ด๋ณด๋ ๋งํผ, ๋ ๋๋ฒ๊น ํ ์๋ก ์๊ฒจ๋๋ ํ์ต์ ๊น์ด์ ๋ถํ์คํ ์ดํด๊ฐ ๋์ด๋ ๊ฒ ๊ฐ์ ์ฌ๊ธฐ์์ ํ๋ฆ์ ์์ฝํ ํ ์ ํฌ ์ฝ๋๋ก ๋์ด๊ฐ๊ฒ ์ต๋๋ค. ํน์๋ ์ ๊ฐ ํ๋ฆฐ ๋ถ๋ถ์ด ์์ ์๋ ์์ผ๋ ์ด ์ ์ฐธ๊ณ ํด ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
์์ฝํ๊ณ ๋นจ๋ฆฌ ์ ํฌ ์ฝ๋๋ก ๋์ด๊ฐ์๋ค..!
๊ฐ๋จํ ์์ฝํ๊ณ ๋น ๋ฅด๊ฒ ์ ํฌ ์ฝ๋๋ก ๋ค์ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
- HttpServletRequest.getSession()์ ์คํํฉ๋๋ค.
- RequestFacade.getSession()์ด ์คํ๋ฉ๋๋ค.
- Request.getSession()์ด ์คํ๋ฉ๋๋ค.
- Request.doGetSession(boolean create)์ด ์คํ๋ฉ๋๋ค.
- Manager.createSession(sessionId: null)์ด ์คํ๋ฉ๋๋ค. ๊ตฌํ์ฒด์ธ ManagerBase๊ฐ ์คํํฉ๋๋ค.
- ManagerBase.generateSessionId()๊ฐ ์คํ๋ฉ๋๋ค.
- SessionIdGenerator.generateSession()๊ฐ ์คํ๋ฉ๋๋ค. ๊ตฌํ์ฒด์ธ StandardSessionIdGenerator๊ฐ ์คํํฉ๋๋ค. ์ค์ ์ ์ธ 16์ง์ ๋ฌธ์์ด์ด ์์ฑ๋ฉ๋๋ค.
- ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure())๊ฐ ์คํ๋์ด ์ฟ ํค๊ฐ ๋ง๋ค์ด์ง๋๋ค. ์ด๋ JSESSIONID๋ SessionConfig.getSessionCookieName(context)๋ก ์ป์ต๋๋ค. (context๋ ์๋ธ๋ฆฟ ์ปจํ ์คํธ๋ฅผ ์๋ฏธํฉ๋๋ค.)
- ๋ง๋ ์ฟ ํค๋ฅผ Response์ ์ ์ฅํ ๋ค, HttpServletRequest.getSession()์ ์คํ์ด ๋๋ HttpSession์ ์ป์ต๋๋ค.
- ๋ง๋ ์ธ์ ์ Store, Manager ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ ์ฅ & ์กฐํ & ์ญ์ & ์ด๊ธฐํ ๋ฑ์ ์ํํ ์ ์์ต๋๋ค. (์ด ๋ถ๋ถ์ ๊ตฌ์ฒด์ ์ธ ๋ด์ฉ์ ๋ ํ์ธํ์ง ๋ชปํ์ต๋๋ค. ๊ทธ๋ฌ๋ 1 ~ 9๋ฒ ๊ณผ์ ์ ํ์คํฉ๋๋ค.)
AuthController
// import ํํ์ ์๋ต
@RequiredArgsConstructor
@RequestMapping("/auth")
@RestController
public class AuthController {
...
private static final String SESSION_ATTRIBUTE_NAME = "AUTH";
private final AuthService authService;
// ์ฟ ํค ๋ฐฉ์์ ์ด์ ๊ธ ์ฐธ๊ณ
@PostMapping("/login/session")
public ResponseEntity<Void> loginWithSession(@RequestBody @Valid final LoginRequest request,
final HttpServletRequest servletRequest) {
HttpSession session = servletRequest.getSession();
Member loginMember = authService.loginWithCookieAndSession(request);
saveSessionValueByMember(session, loginMember);
return ResponseEntity.ok()
.build();
}
private static void saveSessionValueByMember(final HttpSession session, final Member member) {
session.setAttribute(SESSION_ATTRIBUTE_NAME, member.getMemberAuth());
}
}
- HttpServletRequest.getSession์ผ๋ก๋ถํฐ HttpSession์ ์ป์ต๋๋ค.
- ๋ก๊ทธ์ธํ ํ์์ ๊ฐ์ ธ์ต๋๋ค.
- ์ธ์ ์์ ํค๋ฅผ AUTH, ๊ฐ์ ํ์์ MemberAuth (๋๋ค์, ๋น๋ฐ๋ฒํธ) ๊ฐ์ฒด๋ก ๋ฃ์ต๋๋ค.
- ๋ด๋ถ ์ฝ๋๋ฅผ ๋ฏ์ด๋ดค์ ๋ ์๊ฒ ๋ ๊ฒ์ฒ๋ผ, Response ์์ ์ธ์ ์ ๋ฃ์ด์ฃผ๋ฏ๋ก ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ HttpServletResponse๋ฅผ ๋ฃ์ ํ์๊ฐ ์์ต๋๋ค.
AuthService
์๋น์ค๋ ์ฟ ํค ๋ฐฉ์๊ณผ ๋์ผํฉ๋๋ค. ๋ก๊ทธ์ธํ ํ์์ ์กฐํํ๊ณ ๋ฐํํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋๋ค.
์คํ ๊ฒฐ๊ณผ
ํ์๊ฐ์
ํ์๊ฐ์ ๋ํ ์ฟ ํค ๋ฐฉ์๊ณผ ๋์ผํฉ๋๋ค.
๋ก๊ทธ์ธ
๋ก๊ทธ์ธ ์, ์ฟ ํค์ ํค๊ฐ JSESSIONID, ๊ฐ์ด ์ธ์ ID๋ก ๊ธฐ๋ก๋จ์ ๋ณผ ์ ์์ต๋๋ค.
์ ๋ฆฌ
์ธ์ ์ ํฐ์บฃ ์ฝ๋๋ฅผ ๋ด์ผ ํ๋ ๋งํผ, ์ฟ ํค ๋ฐฉ์๋ณด๋ค ๋ ๋ณต์กํ ๊ตฌ์กฐ๋ก ๋์ด ์์ต๋๋ค. ๊ทธ๋์ ์ ๊ฐ ๋ง์ ์ฝ๋๋ค์ ์ฌ๋ ธ์ง๋ง ๊ทธ ๊ณผ์ ์์ ์ดํด๊ฐ ๋์ง ์์ผ์ จ์ ์๋ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
๊ทธ๋ด ๋๋ ๊ธฐ์กด ์ฟ ํค ๋ฐฉ์์ ๋๋ค์, ๋น๋ฐ๋ฒํธ ๋ฑ ๋ฏผ๊ฐํ ์ ๋ณด๋ค์ด ์ ๋ถ ๋ค ๋ค์ด์์๊ธฐ์ ๋ณด์์ ๋ฐ๋ฅธ ์ํ์ด ์กด์ฌํ๊ณ , ์ด๋ฅผ ์กฐ๊ธ์ด๋๋ง ๋ ๋ณด์ํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ผ๋ก ์ธ์ ์ ์ฌ์ฉํ๊ฒ ๋์๋ค๋ ์ฌ์ค์ ๊ธฐ์ตํด ์ฃผ์๋ฉด ๋ฉ๋๋ค. (๋ฌผ๋ก ์ธ์ ๋ํ ์ธ์ ํ์ด์ฌํน ๋ฑ ์๋ฒฝํ ๋ณด์์ ๊ฐ์ง์ง ์์ต๋๋ค.)
๋ง์ฝ ์๋ชป๋ ๋ด์ฉ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ๋๊ธ ๋ถํ๋๋ฆฝ๋๋ค! ๋ฐ๋ก ๋ฐ์ํ๊ฒ ์ต๋๋ค ๐
๋ค์ ๊ธ์์๋ JWT์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค!
Reference
- ์ธ์ vs ํ ํฐ vs ์ฟ ํค? ๊ธฐ์ด๊ฐ๋ ์ก์๋๋ฆผ. 10๋ถ ์์ญ!
- The Context Container - Tomcat docs
- ์ปค๋ฅ์ ํ : java:comp/env
- ์ปค๋ฅ์ ํ (DBCP) oracle
- ๊ฐ์ธ์ ์ผ๋ก ์์ฑํ ์ฟ ํค์ ์ธ์ ์ ๋ํ ๊ธ
- Interface HttpSession - Oracle docs
- ์๋ธ๋ฆฟ์ ์ธ์ ๊ด๋ฆฌ (Servlet Session Management)
- [Tomcat] JNDI(Java Naming and Directory Interface) ์ค์ ์ด ์ ๋ฆฌ
- [์น ์ค์ ] ํฐ์บฃ(tomcat) server.xml ์ค์ ํ๊ธฐ
- ํฐ์บฃ์์๋ ์ด๋ป๊ฒ JSESSIONID๋ฅผ ๋ง๋๋ ๊ฒ์ผ๊น?
- [๊ณต์ง] Apache Tomcat ๋ณด์์ทจ์ฝ์ ์ ๋ฐ์ดํธ
- Apache Tomcat Session Deserialization (CVE-2020-9484) ์ทจ์ฝ์
- java:comp/env
- Class SecurityUtil - Tomcat docs
- Tomcat์ RequestFacade, ResponseFacade
- [TOMCAT] jsession ์ด๋?
- ํฐ์บฃ ์ฌ์์ ํ ์ธ์ ์ ์ง/์ฃฝ์ ์ฒ๋ฆฌ (Persistent Session)
- [Spring] ์ธ์ (Session)์ด๋?
- Spring MVC - Session์ ์ด์ฉํ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
- ์ฟ ํค(Cookie)์ ์ธ์ (Session)์ ์ฐจ์ด (+์บ์(Cache))