본문으로 건너뛰기
김신건의 로그

[Spring Security] Session Management와 Remember-Me

· 수정 · 📖 약 3분 · 1,018자/단어 #spring #security #session #remember-me #csrf
Spring Security Session, SessionManagement, Remember-Me, concurrent session, session fixation, Spring Session, 세션 관리

정의

spring-security 가 JWT / stateless 위주를 다룬다면, 이 페이지는 session-based 인증 의 세부 동작 (생성 정책, 동시 세션 제한, fixation 방어, remember-me) 을 다룬다.

session-based 가 적합한 경우:

  • 단일 서비스, 모놀리스
  • 사용자 수가 manageable
  • 서버에서 강제 로그아웃 / 세션 무효화 필요
  • CSRF 보호 + httpOnly cookie 가 자연스러움

stateless (JWT) 가 적합한 경우:

  • 여러 마이크로서비스 (token 공유)
  • 모바일 / SPA + BFF 없음
  • 무상태 horizontal scaling 우선

대부분 실무는 session + Redis backend (Spring Session) 가 최적 균형.

SessionCreationPolicy 4가지

@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    return http
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
        .build();
}
Policy동작
ALWAYS매 요청마다 세션 생성 (사용자 비-인증도)
IF_REQUIRED (기본)필요 시 (인증 시) 생성
NEVER직접 생성 안 하지만 기존 세션 사용
STATELESS세션 절대 안 만듦 (JWT 환경)

JWT 기반 API 는 반드시 STATELESS. 그래야 JSESSIONID cookie 가 안 생기고 메모리 누수도 방지.

Session Fixation 방어

Session Fixation 공격: 공격자가 알려진 session id 를 피해자에게 심고, 피해자가 그 session 으로 로그인하면 공격자도 같은 session 으로 접근.

Spring Security 기본 방어: 로그인 성공 시 새 session id 발급.

.sessionManagement(s -> s
    .sessionFixation(f -> f.migrateSession())   // 기본
    // .sessionFixation(f -> f.newSession())     // attribute 도 폐기
    // .sessionFixation(f -> f.changeSessionId())// Servlet 3.1+, attribute 유지 + id 변경
    // .sessionFixation(f -> f.none())           // 비활성 (위험)
)

migrateSession 이 가장 보수적 (attribute 도 새 세션으로 복사). changeSessionId 가 가장 효율적 (id 만 교체).

동시 세션 제어 (concurrent session)

같은 사용자가 여러 곳에서 로그인 못 하게:

.sessionManagement(s -> s
    .maximumSessions(1)
    .maxSessionsPreventsLogin(false)   // false 면 새 로그인이 기존 세션 무효화
                                        // true 면 새 로그인 차단
    .expiredUrl("/login?expired"))

HttpSessionEventPublisher Bean 필요 (세션 만료 이벤트 발행):

@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

maximumSessions = 1 + maxSessionsPreventsLogin = false 면:

  • A 컴퓨터 로그인 후 B 컴퓨터 로그인
  • A 의 세션 즉시 만료 → A 에서 다음 요청 시 /login?expired 로 redirect

Remember-Me (자동 로그인)

사용자가 “로그인 유지” 체크하면 cookie 로 장기 유지:

.rememberMe(r -> r
    .key("my-secret-remember-key")         // HMAC 키
    .tokenValiditySeconds(86400 * 14)      // 2주
    .userDetailsService(userDetailsService))

기본은 hash-based: cookie 에 username + expiry + hash(username + expiry + password + key) 저장. 사용자가 비밀번호 바꾸면 자동 무효화.

더 안전한 persistent token 방식:

@Bean
PersistentTokenRepository tokenRepository(DataSource ds) {
    JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(ds);
    return repo;
}

.rememberMe(r -> r
    .tokenRepository(tokenRepository)
    .tokenValiditySeconds(86400 * 14))

DB 에 (series, token, username, last_used) 저장. token rotation: 매번 사용할 때마다 새 token 발급, 도난 시 시리즈 mismatch 감지 가능.

cookie 도난 (XSS) 대비 secure, httpOnly, SameSite=Lax 설정 필수.

Spring Session (분산 세션)

여러 인스턴스가 세션 공유 (Redis / DB 백엔드):

implementation("org.springframework.session:spring-session-data-redis")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
spring:
  session:
    store-type: redis
    timeout: 30m
  data:
    redis:
      host: redis.internal

HttpSession API 그대로 사용. Spring Session 이 backend 만 교체. 인스턴스 죽어도 다른 인스턴스에서 같은 session id 로 계속.

장점:

  • horizontal scaling 시 sticky session 불필요
  • 인스턴스 재시작 시 사용자 안 로그아웃됨
  • 강제 로그아웃 (admin 이 특정 사용자 모든 세션 종료) 가능

함정: 세션 직렬화. session attribute 에 저장하는 객체는 Serializable. 클래스 변경 시 InvalidClassException 위험. JSON serializer 사용 권장 (GenericJackson2JsonRedisSerializer).

세션 정보 활용

@RestController
class SessionController {
    @GetMapping("/session")
    public Map<String, Object> session(HttpSession session, Authentication auth) {
        return Map.of(
            "id", session.getId(),
            "createdAt", new Date(session.getCreationTime()),
            "lastAccessed", new Date(session.getLastAccessedTime()),
            "maxInactive", session.getMaxInactiveInterval(),
            "user", auth.getName()
        );
    }

    @PostMapping("/logout-all")
    public String logoutAll(Authentication auth, FindByIndexNameSessionRepository<?> repo) {
        Map<String, ? extends Session> sessions = repo.findByPrincipalName(auth.getName());
        sessions.keySet().forEach(repo::deleteById);
        return "logged out " + sessions.size() + " sessions";
    }
}

FindByIndexNameSessionRepository 가 사용자별 세션 조회 / 일괄 삭제 API 제공 (Spring Session).

server:
  servlet:
    session:
      cookie:
        same-site: lax    # 또는 strict
        secure: true      # HTTPS 만
        http-only: true   # JS 접근 차단
SameSite동작
Strict다른 사이트 어떤 요청에도 cookie 안 보냄. 강력하지만 외부 링크 클릭 시 로그아웃처럼 보임
Lax (현재 브라우저 기본)top-level navigation (링크) 시만 보냄. 대부분 use case OK
None모든 cross-site 요청에 보냄. CSRF 위험. iframe / 3rd party 통합 시만. Secure 필수

대부분 Lax 권장.

CSRF Token 과 Session

CSRF token 은 보통 session 에 저장 (HttpSessionCsrfTokenRepository). stateless API 면 cookie 기반:

.csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))

cookie 의 XSRF-TOKEN 을 JS 가 읽어 (httpOnly=false) X-XSRF-TOKEN 헤더로 보냄. SPA 패턴.

세션 timeout

server:
  servlet:
    session:
      timeout: 30m
      tracking-modes: cookie   # url 추적 비활성화 (XSS / phishing 방어)

idle timeout: 마지막 요청 후 30분 inactive 면 만료. 보안과 UX 트레이드오프.

absolute timeout (session 생성 후 절대 시간) 은 Spring Security 기본 미지원, 직접 filter 작성:

@Component
class AbsoluteTimeoutFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        HttpSession s = req.getSession(false);
        if (s != null && (System.currentTimeMillis() - s.getCreationTime()) > 8 * 3600 * 1000) {
            s.invalidate();   // 8시간 절대 만료
            resp.sendRedirect("/login?expired");
            return;
        }
        chain.doFilter(req, resp);
    }
}

함정과 베스트 프랙티스

  • API 는 STATELESS, 웹은 IF_REQUIRED: 혼합 시 명시적 분리
  • 세션 직렬화 호환성: 클래스 변경 전 deploy 순서 주의 (rolling)
  • Remember-Me token rotation 권장: persistent token + 도난 감지
  • maximumSessions 는 세션 수: 동시 사용자 수 아님 (같은 사용자 기준)
  • SameSite=None 은 Secure 필수: HTTPS 강제
  • logout 시 session.invalidate + SecurityContextHolder.clearContext : Spring Security 가 처리하지만 직접 logout handler 면 명시
  • session attribute 에 큰 객체 금지: Redis 비용 + 직렬화 시간

관련 위키

이 글의 용어 (6개)
[Java] Servlet API: HttpServletRequest, HttpServletResponsejava
정의 Servlet 은 Java 진영의 표준 HTTP 처리 API. 컨테이너 (Tomcat / Jetty / Undertow) 가 HTTP 요청을 객체 ( ) 로 만들어 serv…
[Java] Servlet Filterjava
정의 Filter 는 servlet 호출 전/후 에 끼어들어 요청과 응답을 가로채는 컴포넌트. 인증, 로깅, 압축, CORS, 인코딩 변환 같은 횡단 관심사 (cross-cutt…
[Spring Security] OAuth2 Authorization Code Flow + PKCEspring
정의 가 OAuth2 Login (소셜 로그인) 의 개요를 다룬다면, 이 페이지는 표준 Authorization Code Grant 의 전체 흐름과 PKCE, refresh to…
[Spring Security] Testing: @WithMockUser, MockMvc, jwt(), oauth2Login()spring
정의 Spring Security 가 적용된 컨트롤러를 테스트할 때 흔한 어려움: - 매번 실제 로그인 절차를 거쳐야 인증된 요청 가능 - CSRF token 이 없으면 POST…
[Spring] RequestContextHolder, LocaleContextHolder: ThreadLocal 기반 요청 컨텍스트spring
정의 Spring 은 HTTP 요청 / 응답 / locale / 트랜잭션 / 인증 같은 요청 범위 데이터를 ThreadLocal 에 저장 해 전역 노출한다. 메서드 인자로 일일이…
[Spring] Security: Filter Chain, JWT, OAuth2spring
정의 Spring Security는 인증·인가의 사실상 표준. 서블릿 필터 체인 위에 다양한 인증/권한 처리를 모듈식으로. 6.0+ 부터 Lambda DSL 표준, Bean 방식…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기