Spring Security + JWT 초기 세팅 회고

오병택·2026년 1월 29일
post-thumbnail

CSRF/세션 설정, CSRF가 필요한 JWT 케이스, AuthenticationManager vs SecurityContextHolder 기준 정리

오늘 한 것 요약

JWT 기반 인증을 붙이기 전에 SecurityConfig로 보안 기본 뼈대를 잡았다.
그리고 “JWT인데도 CSRF를 켜야 하는 경우”가 존재한다는 기준을 정리했고,
마지막으로 헷갈리기 쉬운 포인트인 AuthenticationManager/Provider는 언제 타고, 언제 SecurityContextHolder에 직접 넣는지를 정리했다.

1) SecurityConfig 기본 뼈대 구성

JWT 기반 인증을 붙이기 위한 최소한의 SecurityFilterChain 구성은 아래처럼 시작했다.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(sm ->
                sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated());

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

핵심 의도

  • CSRF 비활성화: JWT를 Authorization 헤더(Bearer) 로 보내는 전형적인 API 구조에서는 CSRF 리스크가 크게 줄어들어 disable을 많이 한다.

  • STATELESS: 서버가 세션에 인증 상태를 저장하지 않고 요청마다 JWT로 인증하는 “무상태 API”를 선언한다.

  • /api/auth/** 허용: 회원가입/로그인/재발급 같은 인증 진입점은 인증 없이 접근 가능해야 한다.

  • 나머지 요청은 인증 필요: 결국 JWT 필터(또는 인증 로직)가 SecurityContext에 인증 정보를 넣어줘야 통과된다.

2) JWT인데도 CSRF를 켜야 하는 경우

오늘 정리한 핵심 결론은 이거였다.

JWT라고 해서 CSRF가 항상 불필요한 건 아니다.
브라우저가 인증 정보를 “자동으로 전송”하는 구조(쿠키 기반)라면 JWT여도 CSRF를 고려해야 한다.

CSRF 방어가 필요해지기 쉬운 대표 케이스

  • JWT(특히 Refresh Token)를 HttpOnly 쿠키로 저장하는 경우

    • 브라우저가 쿠키를 자동 전송 → 공격자가 사용자의 브라우저를 이용해 요청을 보내면 쿠키가 함께 실릴 수 있음 → CSRF 위험
  • Access는 헤더로 보내더라도 Refresh는 쿠키로 운영하는 경우

    • /auth/refresh 같은 재발급 엔드포인트가 CSRF 타겟이 될 수 있어 Origin/Referer 체크, SameSite 정책, CSRF 토큰 등을 함께 고려하는 팀이 많다.

반대로,

  • Access Token을 Authorization 헤더로만 보내고 쿠키 인증을 안 쓰는 순수 API 구조면 보통 CSRF를 꺼두는 편이 흔하다.

3) AuthenticationManager/Provider는 언제 타고, 언제 SecurityContextHolder에 직접 넣을까?

여기가 오늘 가장 헷갈렸던 포인트라 기준을 명확히 잡았다.

3-1. 큰 그림

  • AuthenticationManager/Provider를 탄다
    → “인증 검증(자격 증명 확인)을 시큐리티 표준 파이프라인에 맡긴다”

  • SecurityContextHolder에 직접 담는다
    → “내가 이미 검증했으니 결과만 컨텍스트에 꽂는다”

3-2. AuthenticationManager/Provider를 거치는 대표 케이스

(1) 로그인(아이디/비밀번호) 검증

가장 전형적인 사용처다.

  • AuthenticationManager.authenticate(...) 호출

  • 내부 Provider(예: DaoAuthenticationProvider)가

    • UserDetailsService로 사용자 로딩

    • PasswordEncoder로 비밀번호 검증

    • 계정 잠금/비활성 같은 정책 처리

  • 성공 시 authenticated Authentication 반환

✅ 정리: “비밀번호 같은 자격 증명을 검증해야 하면 AuthenticationManager를 타는 게 자연스럽다.”

(2) 인증 방식을 Provider로 통일하고 싶을 때(선택)

JWT도 Provider로 처리하도록 설계할 수 있다.

  • JwtAuthenticationToken 생성 → AuthenticationManager.authenticate(token)

  • JwtAuthenticationProvider에서 검증 + 권한 구성 → authenticated 반환

언제 유리하냐?

  • 인증 방식이 여러 개로 늘어날 가능성이 크고(확장성)

  • 인증 로직/예외 처리/감사로그를 중앙화하고 싶을 때

3-3. SecurityContextHolder에 직접 담는 대표 케이스

(1) JWT 필터에서 요청마다 검증하는 방식(가장 흔함)

JWT 기반 API에서 흔히 이렇게 한다.

  1. 토큰 추출

  2. 서명/만료 검증

  3. (필요시) 사용자/권한 로딩

  4. Authentication 생성 후 SecurityContextHolder에 set

왜 이 방식이 흔하냐?

  • JWT는 “비밀번호 확인”이 아니라 “서명/만료 검증” 중심

  • 매 요청마다 Provider 체인을 태우지 않아도 되고 구현이 단순하다

3-4. 실무에서 가장 많이 쓰는 조합(추천)

JWT + Stateless API에서는 보통 아래 조합이 가장 흔하고 무난하다.

  • 로그인(아이디/비밀번호): AuthenticationManager 사용

  • 로그인 이후 요청(JWT): 필터에서 검증 후 SecurityContextHolder 직접 세팅

3-5. 직접 세팅 방식에서 주의할 점(자주 겪는 실수)

  • 필터에서 예외가 터졌을 때 401/403 응답이 지저분해짐
    → EntryPoint/AccessDeniedHandler 또는 필터 예외 흐름 정리 필요

  • 매 요청마다 사용자 조회로 DB 부하가 커질 수 있음
    → 토큰 클레임에 role 포함/캐싱 고려

  • 비동기/스레드 전환 시 컨텍스트 유지 문제
    → 요청 스레드 밖으로 넘어가면 별도 처리 필요

profile
걱정하지 말고 일단 해봐!

0개의 댓글