이전 글에 이어서 바로 세션을 이용한 로그인도 구현해 보겠습니다! (회원 식별은 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))
'🚀 팁 (기술 적용 방법 등)' 카테고리의 다른 글
[Github] Github의 Issue와 PR (Pull Request) 알아보기 (PR 병합 후 이슈가 자동으로 닫히게 하려면?) (0) | 2024.05.18 |
---|---|
[Spring REST Docs ✍️] 어렵게만 느껴졌던 REST Docs를 적용해보자! (2) (0) | 2024.03.05 |
[Spring MVC 🌐] 회원 식별을 해 보자! (1) - 쿠키 적용 방법 🍪 (0) | 2024.02.08 |
[Spring REST Docs ✍️] 어렵게만 느껴졌던 REST Docs를 적용해보자! (1) (1) | 2024.02.07 |