가장 오래된 방법부터 — 폼 로그인과 세션 인증
스프링 시큐리티를 아무 설정 없이 의존성에만 추가하고 앱을 켜면, 모든 API가 로그인 페이지로 리다이렉트되기 시작합니다. 이게 기본값입니다. formLogin()이 자동으로 켜져 있고, 콘솔에는 매번 바뀌는 임시 비밀번호가 로그로 찍힙니다. 실무에서 이 상태로 쓸 일은 없지만, 세션 기반 인증이 어떻게 동작하는지 보기엔 가장 빠른 방법입니다.
세션 인증은 사실 서블릿 컨테이너의 기능이다
스프링 시큐리티가 세션을 발명한 게 아닙니다. HttpSession은 서블릿 스펙에 원래 있던 개념이고, 스프링 시큐리티는 그 위에 "인증된 사용자 정보를 세션의 특정 위치에 넣고 꺼내는" 규칙을 얹었을 뿐입니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/signup").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home", true)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
);
return http.build();
}로그인이 성공하면 스프링 시큐리티는 Authentication 객체를 SecurityContext에 담고, 이 SecurityContext를 HttpSession에 저장합니다. 다음 요청부터는 브라우저가 보내주는 세션 쿠키(JSESSIONID)로 세션을 찾고, 그 안에서 SecurityContext를 복원합니다. 서버 입장에서 로그인 상태를 "기억"하는 건 결국 이 세션 저장소입니다.
세션 고정 공격과 스프링 시큐리티의 기본 대응
세션 기반 인증을 직접 구현한다면 놓치기 쉬운 문제가 세션 고정(session fixation) 공격입니다. 공격자가 로그인 전의 세션 ID를 피해자에게 심어두고, 피해자가 그 세션으로 로그인하면 공격자도 같은 세션 ID로 로그인 상태를 얻게 되는 방식입니다. 스프링 시큐리티는 기본적으로 로그인이 성공하는 시점에 세션 ID를 새로 발급합니다(changeSessionId 전략). 별도로 설정할 필요가 없습니다 — 오히려 이걸 끄는 설정(sessionFixation().none())을 실수로 켜지 않도록 주의하는 쪽이 중요합니다.
동시 세션을 제한하고 싶다면(한 계정으로 동시 로그인 2대까지만 허용한다든가) sessionManagement에 옵션을 추가합니다.
.sessionManagement(session -> session
.maximumSessions(2)
.maxSessionsPreventsLogin(false) // true면 3번째 로그인을 거부, false면 가장 오래된 세션을 만료시킴
)이 장에서 다루지 않는 것
세션 기반 인증은 여기서 다룬 게 사실상 전부입니다. 개념 자체가 단순하기 때문입니다. 세션 저장소를 여러 서버가 공유해야 하는 문제(로드밸런서 뒤에 인스턴스가 여러 대일 때 세션을 어디에 둘 것인가)는 스프링 시큐리티의 범위를 벗어나는 인프라 문제라 이 책에서는 다루지 않습니다. Redis 같은 외부 세션 저장소를 붙이는 spring-session 프로젝트를 검색해보시길 권합니다.
세션 방식의 진짜 한계는 공격이 아니라 확장성 쪽에서 옵니다. 모바일 앱이나 별도 프런트엔드 서버가 API를 호출하는 구조에서는 쿠키 기반 세션이 잘 맞지 않습니다. 그 이야기는 5장에서 JWT로 이어집니다.