Spring Security는 서블릿 필터들의 사슬로 동작한다. 컨트롤러에 닿기 전에 인증·인가가 끝난다.
어디에 끼어 있나
요청 → [ ... 서블릿 필터들 ... ]
└ DelegatingFilterProxy → FilterChainProxy
└ SecurityFilterChain (여러 필터)
→ DispatcherServlet → 인터셉터 → 컨트롤러
DispatcherServlet 이전이라는 게 중요하다 — 그래서 @RestControllerAdvice가 보안 필터에서 난 예외는 못 잡는다.
주요 필터 순서
SecurityContextPersistenceFilter 저장된 인증 정보를 꺼내 SecurityContext 에 넣는다 UsernamePasswordAuthenticationFilter 폼 로그인 처리 (커스텀 JWT 필터를 보통 여기쯤 끼운다)
- ExceptionTranslationFilter — 인증 예외 → 401 · 인가 예외 → 403 으로 변환
- AuthorizationFilter — 최종 인가 판단
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // 아래 참고
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(a -> a
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
401과 403을 구분한다
- 401 Unauthorized — 누구인지 모른다 (인증 실패·토큰 없음)
- 403 Forbidden — 누구인지는 아는데 권한이 없다 (인가 실패)
세션 vs JWT
| 세션 | JWT | |
|---|---|---|
| 상태 | 서버가 보관 | 무상태 |
| 확장 | 세션 저장소 공유 필요(Redis) | 서버 간 공유 불필요 |
| 무효화 | 즉시 가능 | 어렵다 — 만료까지 유효 |
| 크기 | 쿠키에 id만 | 토큰 자체가 크다 |
JWT의 결정적 약점은 무효화다. 로그아웃·권한 회수·탈취 대응이 어렵다.
실무 절충
- 액세스 토큰은 짧게 (수 분~수십 분)
- 리프레시 토큰은 서버에 저장해 회수 가능하게
- 리프레시 재사용이 감지되면 해당 사용자의 토큰을 전부 무효화
CSRF는 왜 끄고 언제 켜나
-
CSRF 공격은 '브라우저가 쿠키를 자동으로 붙인다' 는 성질을 노린다
-
쿠키 기반 세션 인증 (폼 로그인·SSR) — → CSRF 보호를 켠다
-
Authorization 헤더로 토큰 전달 — → 자동 전송이 아니므로 위험이 없다 → 꺼도 된다
"REST API니까 끈다"가 아니라 "인증 정보를 쿠키로 자동 전송하지 않으니 끈다" 가 정확한 이유다. 토큰을 쿠키에 담는다면 다시 켜야 한다.
면접 함정
- ❌ "JWT가 세션보다 안전하다" → 무효화가 어려워 오히려 탈취 대응이 약하다. 무상태 확장성이 장점이다.
- ❌ "CSRF는 항상 끈다" → 쿠키 인증이면 반드시 켠다.