이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › 보안
세션은 무엇이고, 로그인한 상태는 어디에 기억되는가
세션은 서버 안의 저장소에 적어 두는 기억입니다. 코드 칸의 session은 HttpSession이라는 타입의 인자이고, 서버의 세션 저장소에서 이 요청에 딸린 칸 하나를 가리킵니다. login 메서드의 session.setAttribute("user", "kim") 줄은 그 칸에 "이 세션의 사용자는 kim"이라고 적는 줄이고, me 메서드의 session.getAttribute("user") 줄은 칸에서 그 값을 읽는 줄입니다. 칸마다 서버가 붙인 번호(세션 id)가 있고, 서버는 이 번호만 JSESSIONID라는 이름의 쿠키로 브라우저에 줍니다. 브라우저는 다음 요청마다 그 쿠키를 보내고, 서버는 번호로 저장소의 칸을 찾아 누구인지 압니다. 이 실습은 로그인한 뒤에 /me를 요청하는 방법을 바꿔 가며(쿠키 없이, 다른 사람이, 서버가 칸을 지운 뒤에), 서버가 누구인지 알아보는지를 봅니다. 코드 칸에는 로그인에 성공했을 때 실행되는 부분만 있고, 아이디와 비밀번호를 확인하는 과정은 이 실습에서 다루지 않아 줄였습니다. 쿠키가 무엇인지는 앞의 "쿠키" 실습에서 다룹니다.
실행으로 대조 · 스프링 부트 4.1.1(내장 톰캣 11.0.24)에서 여덟 가지 조합을 실제 컴포넌트 스캔으로 띄우고 요청을 보내 대조(HttpClient가 Cookie 헤더를 직접 적음, 실제 브라우저에서는 실행하지 않음)
@PostMapping("/login")
public String login(HttpSession session) {
// 서버의 세션 저장소에 "이 세션의 사용자는 kim"이라고 적어 둔다
session.setAttribute("user", "kim");
return "로그인됨";
}
@GetMapping("/me")
public String me(HttpSession session) {
// 요청에 실려 온 세션 번호로 저장소의 칸을 찾아 읽는다
Object user = session.getAttribute("user");
return user == null ? "누구인지 모름" : "안녕하세요, " + user;
}
스프링 부트 4.1.1(스프링 프레임워크 7.0.9, 내장 톰캣 11.0.24)을 JDK 21에서 실행한 결과입니다. 여덟 가지 조합을 모두, 화면의 코드와 같은 소스(화면에 없는 logout 메서드 하나만 더함)를 패키지 app으로 컴파일하고 시작 클래스(@SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan)와 함께 자바 프로세스를 새로 띄워 확인했습니다. 컨트롤러를 손으로 등록하지 않고 스프링이 클래스를 훑어 찾게 했으며, 요청은 서버가 뜬 뒤 실제 HTTP로 보냈습니다. 클래스패스에는 스프링 시큐리티, JPA, H2를 넣지 않았습니다(시큐리티가 있으면 요청이 막히고 세션 처리가 달라질 수 있기 때문입니다). 화면의 코드는 package와 import 줄, 클래스 선언, 로그아웃 메서드를 줄였습니다. 로그아웃 메서드는 session.invalidate()를 실행하고 "로그아웃됨"을 돌려주는 메서드이고, 아이디와 비밀번호를 확인하는 과정도 이 실습에서 다루지 않아 줄였습니다. 대조한 것은 login 직후 서버의 세션 저장소(톰캣 세션 관리자의 세션 수와 칸마다 적힌 내용), login 응답의 Set-Cookie, /me 응답입니다. 같은 브라우저는 받은 Set-Cookie의 값을 다음 요청의 Cookie 헤더에 직접 적는 클라이언트 하나이고(쿠키 보관 기능을 쓰지 않았고 실제 브라우저에서는 실행하지 않았습니다), 다른 사람은 별개의 연결(별개의 클라이언트)이 같은 값을 Cookie 헤더에 적어 보내는 것이며, 서버가 세션을 지우는 일은 같은 쿠키로 POST /logout을 먼저 보내는 것으로 재현했습니다. 화면의 번호(3A9F…, B71E…)는 설명을 위해 지어 낸 예시이고, 실제로는 실행할 때마다 다른 영문 대문자와 숫자 32글자였습니다. 문서가 정한 것은 Jakarta Servlet 6.1 명세 7장(7.1.1절: 세션 추적 쿠키의 표준 이름은 JSESSIONID, 7.2절: 세션은 클라이언트가 추적 정보를 서버에 돌려보내 합류하기 전까지 새 세션이고 id는 getId()로 읽는다, 7.5절: 무효화가 시작되면 새 요청은 그 세션을 볼 수 없다)와 HttpSession의 API 문서(세션은 한 사용자를 여러 요청에 걸쳐 식별하고 정보를 저장하는 방법이고, getId의 식별자는 서블릿 컨테이너가 정하며 구현에 따라 다르다)입니다. 다음은 문서가 정한 것이 아니라 이 버전에서 관찰한 동작입니다. 번호가 영문 대문자와 숫자 32글자이고 쿠키 값에 kim이 들어 있지 않다는 것, 쿠키에 Path=/와 HttpOnly 속성이 붙는다는 것, session 인자를 받는 메서드는 setAttribute를 부르지 않아도 세션을 만들고(스프링이 그 인자를 채우려고 요청의 getSession()을 부릅니다. 이 호출은 스프링의 해당 클래스 바이트코드로 확인했습니다) 쿠키가 나간다는 것, 쿠키 없이 /me를 요청하면 서버가 빈 세션을 새로 만들고 새 번호를 준다는 것, 같은 번호를 별개의 연결에서 보내도 서버가 같은 세션으로 다루고 새로 만들지 않는다는 것(두 요청의 연결 포트는 달랐습니다), 서버가 지운 세션의 번호로 요청하면 서버가 새 세션을 만들어 새 번호를 준다는 것, 로그아웃 응답에는 쿠키를 지우라는 Set-Cookie 줄이 없다는 것입니다. 세션 번호를 훔쳐 가는 공격과 방어(세션 고정, HttpOnly 설정 등)는 이 실습에서 다루지 않았고, 공부 쪽 실습("세션과 JWT", "세션 하이재킹과 세션 고정")으로 넘깁니다. 세션의 유효 시간, 여러 서버에서 세션을 나누는 방법, 쿠키를 쓰지 않는 방법(URL에 번호를 싣는 방식 등)도 다루지 않았습니다.
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.