Spring Security에서 미인증 요청이 401이 아닌 403으로 반환된 이유 (Http403ForbiddenEntryPoint)

오병택·2026년 2월 12일
post-thumbnail

0. 상황 설명

이력서를 정리하던 도중 내가 이력서에 쓴 것이 그때 당시엔 확실히 이해하고 있었다.
지금에서 다시보니 이게 왜 발생했더라? 생각이 들어서 다시 한번 재현해보고자 했습니다.
재현하면서 다시 정리하는 상황입니다.

1. 문제 상황

Spring Security 설정에서 아래처럼 모든 요청을 인증 필요로 보호하고 있었다.

.anyRequest().authenticated()

의도한 동작은 다음과 같았다.

  • 로그인(토큰) 없음 → 401 Unauthorized

  • 로그인은 했지만 권한 부족 → 403 Forbidden

하지만 실제로 Postman에서 토큰 없이 보호된 API를 호출했을 때 401이 아닌 403이 반환되었다.

2. 원인 파악 과정

2-1. SecurityContext에 Anonymous 객체가 들어오는지 확인

Spring Security는 인증되지 않은 요청이라도 내부적으로 AnonymousAuthenticationFilter가 동작하여
SecurityContext에 AnonymousAuthenticationToken을 넣을 수 있다.

이를 확인하기 위해 Authentication 객체 타입을 출력해보았고, 실제로 AnonymousAuthenticationToken이 확인되었다.

즉, “인증 객체가 아예 없는 상태(null)”이 아니라
“anonymous 인증 객체가 들어있는 상태”였다.

3. 왜 403이 반환되었는가?

Spring Security에서 예외는 크게 두 가지로 나뉜다.

3-1. 인증 실패(Authentication Failure)

  • 로그인하지 않음

  • 토큰이 없음 / 잘못됨

→ 보통 AuthenticationEntryPoint가 처리
→ REST API 기준으로는 401 반환이 일반적

3-2. 인가 실패(Authorization Failure)

  • 로그인은 했지만 권한이 부족함

→ AccessDeniedHandler가 처리
→ 403 반환

4. 핵심 원인: 기본 EntryPoint가 Http403ForbiddenEntryPoint로 동작

  • EntryPoint/AccessDeniedHandler를 별도로 설정하지 않았기 때문에
    Spring Security는 기본 예외 처리 흐름을 사용했다.

  • 이 과정에서 인증 실패 상황에서 기본 EntryPoint가
    Http403ForbiddenEntryPoint로 잡히며, 미인증 요청임에도 403이 반환되는 현상이 발생했다.

즉, 원인은 다음과 같이 정리할 수 있다.

  • 인증/인가 예외 처리 흐름을 명확히 분리하지 않음

  • 기본 AuthenticationEntryPoint가 Http403ForbiddenEntryPoint로 동작

결과적으로 401이 아닌 403 반환

5. OAuth2Login 설정이 있을 때 200 + HTML이 반환된 이유

처음에는 .oauth2Login(...) 설정이 포함되어 있었다.

OAuth2Login을 활성화하면 Spring Security는 인증이 필요한 요청에서
로그인 페이지로 리다이렉트하는 웹 로그인 플로우를 사용할 수 있다.

따라서 Postman에서 보호된 API를 호출했을 때:

  • 실제로는 302 Redirect 발생

  • Postman이 리다이렉트를 따라가면서 로그인 HTML을 반환

  • 최종적으로 200 + HTML이 출력되는 현상이 발생했다.

  • .oauth2Login()을 주석 처리하자 리다이렉트 흐름이 사라지고,
    403 반환 현상이 명확히 확인되었다.

6. 해결 방법: EntryPoint / AccessDeniedHandler 명시 설정

REST API에서는 인증 실패(401)와 인가 실패(403)를 명확히 분리하는 것이 중요하다.

따라서 아래처럼 EntryPoint와 AccessDeniedHandler를 직접 설정하여 응답을 표준화했다.

이렇게 처리하면:

  • 토큰 없음 / 미로그인 → 401

  • 권한 부족 → 403

으로 정확히 구분된다.

7. 결과

EntryPoint와 DeniedHandler를 명시적으로 구성한 뒤:

  • 인증 실패(401)와 인가 실패(403)가 명확히 분리됨

  • API 응답 코드가 일관되게 유지됨

  • 프론트/클라이언트에서 예외 처리 로직이 단순해짐

  • 디버깅 및 유지보수 효율이 향상됨

최종 정리

문제

  • anyRequest().authenticated()로 보호된 API 호출 시 미인증 요청이 401이 아닌 403 반환

원인

  • 인증/인가 예외 처리 미분리로 기본 EntryPoint인 Http403ForbiddenEntryPoint가 적용됨

해결

  • AuthenticationEntryPoint/AccessDeniedHandler 커스터마이징으로 401/403 응답 분리 및 표준화

효과

  • 인증/인가 응답 코드 표준화로 클라이언트 처리 안정성 및 유지보수성 향상
profile
걱정하지 말고 일단 해봐!

0개의 댓글