본문 바로가기
Java & Spring

세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리

by yamoojin83 2025. 10. 12.

세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리

웹(서버 렌더링)과 API(SPA/모바일)는 인증 상태를 유지하는 방식이 다릅니다. 전통적인 세션 기반 로그인은 서버 세션과 쿠키에 의존하고, Stateless API는 토큰(JWT 등)으로 상태를 유지합니다. 이 글은 두 방식을 서로 다른 SecurityFilterChain으로 분리해 구성하는 실전 패턴을 설명합니다.

1) 핵심 비교

  • 세션 기반: 서버 메모리/스토리지에 세션 상태 저장, CSRF 보호 필요, Remember-me 옵션 사용 가능
  • Stateless: 세션 상태 없음, CSRF 비활성, Bearer 토큰으로 인증(만료·회전 정책 포함)

2) 두 개의 SecurityFilterChain


@Configuration
@EnableMethodSecurity
public class DualChainSecurity {

  @Bean @Order(1)
  SecurityFilterChain api(HttpSecurity http) throws Exception {
    http.securityMatcher("/api/**")
        .csrf(csrf -> csrf.disable())
        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth -> oauth.jwt()); // 또는 httpBasic/bearerResolver 커스터마이즈
    return http.build();
  }

  @Bean @Order(2)
  SecurityFilterChain web(HttpSecurity http, PersistentTokenRepository ptr) throws Exception {
    http
      .authorizeHttpRequests(auth -> auth
          .requestMatchers("/", "/login", "/css/**", "/js/**").permitAll()
          .anyRequest().authenticated())
      .formLogin(fl -> fl
          .loginPage("/login")
          .defaultSuccessUrl("/")
          .failureUrl("/login?error"))
      .rememberMe(rm -> rm
          .tokenRepository(ptr)
          .tokenValiditySeconds(60 * 60 * 24 * 14) // 14일
          .userDetailsService(new AppUserDetailsService(/* inject */)))
      .sessionManagement(sm -> sm
          .sessionFixation(sf -> sf.migrateSession())
          .maximumSessions(1) // 동시 세션 1개 제한
          .expiredUrl("/login?expired"))
      .csrf(csrf -> csrf.ignoringRequestMatchers("/webhook/**")); // 필요한 경로만 제외
    return http.build();
  }
}

3) Remember-me 토큰 저장소(JDBC)


@Configuration
public class RememberMeConfig {
  @Bean
  public PersistentTokenRepository persistentTokenRepository(DataSource ds) {
    JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(ds);
    // 최초 1회: repo.setCreateTableOnStartup(true);
    return repo;
  }
}

Remember-me는 자동 로그인용 장기 쿠키입니다. 보안을 위해 HTTPS + Secure + HttpOnly를 기본으로 사용하세요.

4) 세션 보안 포인트

  • 세션 고정 방지: 로그인 성공 시 migrateSession()로 세션 ID 교체
  • 동시 세션 제한: 계정 공유/탈취 감지에 유용
  • 세션 스토리지: 대규모 서비스는 Spring Session + Redis로 외부화

5) 쿠키 SameSite & 보안 속성

Boot 3.x는 서버 설정만으로 SameSite 제어가 가능합니다.


server:
  servlet:
    session:
      cookie:
        secure: true
        http-only: true
        same-site: lax   # SPA/OAuth 리디렉트 시에는 "none"(+ Secure) 고려

6) 로그아웃/토큰 무효화

  • 세션 기반: http.logout()으로 세션 무효화 + 쿠키 삭제
  • Stateless: 서버 저장소(예: Redis)에 리프레시 토큰을 저장하고, 로그아웃 시 해당 토큰 폐기

7) SPA & API 조합 현실 팁

  • 원 도메인이라면 SameSite=Lax 쿠키로도 무난히 동작
  • 서브도메인 간 교차라면 SameSite=None + Secure + CORS 구성이 필요
  • API는 401/403을 명확히 구분, 프런트는 401에서 로그인 라우팅

8) CORS(선택)


@Configuration
public class CorsCfg {
  @Bean
  CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration c = new CorsConfiguration();
    c.setAllowedOrigins(List.of("https://app.example.com"));
    c.setAllowedMethods(List.of("GET","POST","PUT","DELETE"));
    c.setAllowedHeaders(List.of("*"));
    c.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource s = new UrlBasedCorsConfigurationSource();
    s.registerCorsConfiguration("/**", c);
    return s;
  }
}

9) 선택 가이드

  • 서버 템플릿(Thymeleaf 등) 위주의 웹: 세션 기반 + CSRF 유지 + Remember-me
  • SPA/모바일 API: Stateless + Bearer(JWT) + CSRF 비활성 + 토큰 회전
  • 두 세계가 공존하면 다중 체인으로 분리

 

 

👉 1편: SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기

👉 2편: PasswordEncoder와 회원가입: BCrypt·Pepper·비밀번호 정책

👉 3편: AuthenticationProvider & UserDetailsService 커스터마이징

👉 4편: 세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리

👉 5편: JWT 심화: 만료/클레임/키 회전 + 4편과의 연결 전략

👉 6편: OAuth2 로그인(Google/GitHub) + JWT 브릿지

👉 7편: 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한

👉 8편: CORS·XSS·헤더 보안: SPA/REST 현실 설정

👉 9편: 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway)

👉 10편: 감사 로깅/Audit: 로그인 실패·권한거부 탐지 규격