목차
JSP/Servlet에서 쿠키와 세션을 구분하기
장바구니에 담은 상품을 다음 요청에서도 보여주려면 서버는 “이 요청이 누구의 상태를 가리키는가”를 알아야 한다. HTTP 요청은 매번 독립적으로 들어오므로 상태를 연결하는 식별자나 데이터를 따로 주고받는다. 쿠키는 브라우저가 요청에 함께 보내는 작은 값이고, 세션은 보통 서버가 식별자와 연결해 보관하는 상태다. 세션 식별자는 흔히 쿠키로 오지만, 두 개념을 같은 저장 공간으로 생각하면 보안 설계를 잘못하기 쉽다.
sequenceDiagram participant B as 브라우저 participant S as Servlet participant Store as 세션 저장소 B->>S: 로그인 요청 S->>Store: 인증된 사용자 ID 저장 S-->>B: Set-Cookie: 세션 ID B->>S: Cookie: 세션 ID + 다음 요청 S->>Store: 세션 ID로 상태 조회 S-->>B: 개인화된 응답
그림에서 브라우저에 들어가는 것은 대개 세션 ID다. 사용자 정보나 권한 전체를 쿠키에 넣는 구조가 아니다. 쿠키는 사용자가 읽거나 바꾸거나 지울 수 있으므로 서버는 쿠키 값을 신뢰하지 말고 검증한다. 세션 저장소가 메모리라면 서버 재시작과 여러 서버 인스턴스 배포 때 상태 유지 방식을 별도로 정해야 한다.
쿠키: 선호 언어 하나 저장하기
아래 예제는 Jakarta Servlet API 기준이다. 로그인 토큰이 아니라 ko 또는 en이라는 낮은 민감도의 선호 언어만 저장한다. HTTP 응답의 쿠키 값은 사용하는 Servlet 컨테이너가 허용하는 문자 규칙을 따라야 하므로 예시 값은 ASCII로 둔다. 운영 서비스는 HTTPS에서만 전달하도록 Secure를 설정한다.
Cookie language = new Cookie("language", "ko");
language.setPath("/");
language.setMaxAge(7 * 24 * 60 * 60); // 7일
language.setSecure(true); // HTTPS 전용
language.setHttpOnly(true); // JavaScript 접근 차단
language.setAttribute("SameSite", "Lax"); // Servlet 6.1 기준
response.addCookie(language);
Path=/이면 사이트 전체 요청에 쿠키가 전송된다. 더 좁은 경로에서만 쓰는 값은 그 경로로 제한한다. 도메인 범위도 필요한 곳으로만 설정한다. HttpOnly는 브라우저 스크립트가 쿠키를 직접 읽지 못하게 하지만, XSS 자체를 없애지는 않는다. SameSite도 CSRF 방어를 보조할 뿐, 상태 변경 요청의 CSRF 보호를 대신하지 않는다.
쿠키를 읽을 때 getCookies()가 null일 수 있다. 여러 쿠키 중 같은 이름의 값을 찾더라도 허용 목록을 검사한다.
String language = "ko";
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("language".equals(cookie.getName())) {
String value = cookie.getValue();
if ("ko".equals(value) || "en".equals(value)) {
language = value;
}
break;
}
}
}
request.setAttribute("language", language);
JSP에서 값을 표시할 때는 스크립틀릿 out.println(...)으로 사용자 입력을 HTML에 바로 연결하지 않는다. <c:out value="${language}"/> 같은 이스케이프되는 출력 방식을 사용한다. 이 값은 이미 허용 목록을 통과했지만, 다른 쿠키 값도 같은 방식으로 표시하는 습관이 XSS 위험을 줄인다.
쿠키를 지울 때는 원래의 이름과 Path, 필요하면 Domain을 맞춰서 만료시킨다. 이름만 같고 Path가 다르면 다른 쿠키가 남을 수 있다.
Cookie expired = new Cookie("language", "");
expired.setPath("/");
expired.setMaxAge(0);
expired.setSecure(true);
expired.setHttpOnly(true);
response.addCookie(expired);
브라우저마다 저장 한도와 만료 처리에는 차이가 있다. “쿠키는 1.2MB까지 저장된다” 같은 고정 용량을 전제하지 않는다. 큰 데이터나 민감한 정보를 쿠키에 넣지 않는다.
세션: 로그인 상태는 서버에서 판단하기
인증에 성공한 뒤 서버가 사용자 ID를 세션에 넣는 흐름은 다음과 같다. 인증 판정 자체는 예제 밖의 서비스가 수행한다. 기존 세션 ID를 그대로 높은 권한으로 승격하지 않도록 로그인 시 ID를 교체한다.
// credential 검증이 성공한 뒤에만 실행
HttpSession session = request.getSession();
request.changeSessionId();
session.setAttribute("userId", authenticatedUserId);
session.setMaxInactiveInterval(30 * 60); // 비활성 30분
setMaxInactiveInterval은 마지막 요청 이후의 비활성 시간을 정한다. 브라우저 창을 닫는 순간 서버 세션이 반드시 즉시 삭제되는 것은 아니다. 세션 데이터는 무제한이 아니며, 많은 사용자에게 큰 객체를 저장하면 서버 메모리나 외부 세션 저장소를 압박한다. 필요한 작은 식별자만 저장하고 프로필 같은 최신 정보는 필요한 시점에 조회한다.
보호된 페이지에서는 getSession(false)로 기존 세션만 조회한다. 인증되지 않은 요청을 검사하는 중 새 세션을 만들지 않도록 한다.
HttpSession existing = request.getSession(false);
Object userId = existing == null ? null : existing.getAttribute("userId");
if (!(userId instanceof Long)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
// 여기서부터 사용자 ID에 해당하는 리소스 접근 권한을 추가 확인
로그아웃은 서버 세션을 invalidate()한다. 화면의 “로그아웃 완료” 문구만 바꾸거나 브라우저 쿠키만 지워서는 서버의 세션 상태가 남을 수 있다. 서버 세션이 삭제되었는지, 이후 같은 쿠키로 보호된 페이지에 접근하면 거부되는지 확인한다.
실패 사례와 운영 선택
| 현상 | 확인할 부분 |
|---|---|
| 매 요청마다 로그아웃됨 | 쿠키의 Path·Domain·Secure, HTTPS, 세션 저장소 |
| 로그인 전후 세션 ID가 동일 | 로그인 후 ID 교체와 세션 고정 방어 |
| 로그아웃 뒤에도 접근 가능 | 세션 무효화와 인가 검사 누락 |
| JSP에 태그나 스크립트가 그대로 실행됨 | 출력 이스케이프, 사용자 값의 직접 출력 |
| 서버를 늘린 뒤 세션이 사라짐 | 인스턴스별 메모리 세션과 공유 저장 방식 |
여러 서버가 세션을 공유해야 하면 외부 세션 저장소나 인증 구조를 선택한다. 어느 방식을 쓰든 요청마다 권한을 검사하고, 세션 ID가 담긴 쿠키에는 Secure, HttpOnly, SameSite 정책을 적용한다. 운영 환경의 프록시·TLS 종료 지점도 쿠키 설정에 영향을 준다. CSRF 방어, 세션 만료, 로그아웃 경로를 한 묶음으로 테스트한다.
참고: Jakarta Servlet 6.1 사양, HttpSession API, OWASP 세션 관리, OWASP CSRF 방어.