
Spring Security 401 403 차이는 인증과 인가를 나눠 보면 훨씬 단순해집니다. 401은 대체로 인증이 되지 않은 요청이고, 403은 인증은 되었지만 해당 리소스에 접근할 권한이 부족한 요청입니다.
핵심은 토큰이 없거나 틀린 문제와, 권한이 부족한 문제를 같은 에러로 보지 않는 것입니다. 이 글은 Spring Security 요청 흐름 안에서 401과 403이 어디서 갈라지는지, JWT 설정에서 어떤 부분을 확인해야 하는지 실제 디버깅 순서로 정리합니다.

Spring Security 401 403 차이의 출발점
HTTP 상태 코드만 보면 401과 403은 둘 다 실패처럼 보입니다. 하지만 원인은 다릅니다. 401은 서버가 요청자를 아직 신뢰할 수 없다는 뜻에 가깝고, 403은 요청자가 누구인지는 알지만 그 작업을 허용할 수 없다는 뜻에 가깝습니다.
이 차이를 놓치면 JWT가 잘못됐는지, 권한 매핑이 잘못됐는지, SecurityFilterChain의 matcher 순서가 잘못됐는지 계속 헷갈립니다. 그래서 먼저 요청 흐름을 인증 단계와 인가 단계로 나누어 봐야 합니다.
인증 실패와 권한 거부
- 인증 실패: 토큰이 없거나, 만료됐거나, 서명이 틀렸거나, SecurityContext에 인증 객체가 들어가지 않은 상황
- 권한 거부: 인증 객체는 있지만 필요한 role, authority, scope가 부족한 상황
- 설정 문제: permitAll, authenticated, hasRole, requestMatchers 순서가 의도와 다르게 적용된 상황
예를 들어 `/admin` API에 접근할 때 토큰이 없으면 401이 자연스럽습니다. 반대로 일반 사용자 토큰은 있지만 관리자 권한이 없으면 403이 자연스럽습니다.
요청 흐름으로 보는 401
JWT 기반 API에서 401은 보통 인증 정보를 만들기 전 단계에서 발생합니다. Authorization 헤더가 없거나, Bearer 토큰 형식이 아니거나, 토큰 파싱에 실패하면 SecurityContext에 정상 인증 객체가 들어가지 않습니다.
GET /api/me
Authorization: Bearer 잘못된토큰
1. JWT 필터가 토큰을 읽는다
2. 서명, 만료, 형식을 검증한다
3. 검증 실패로 Authentication을 만들지 못한다
4. 보호된 리소스 접근에서 인증 필요 판단
5. AuthenticationEntryPoint가 401 응답을 만든다이때 봐야 할 것은 권한 목록이 아닙니다. 토큰이 실제로 읽혔는지, 검증 실패가 발생했는지, SecurityContextHolder에 Authentication이 들어갔는지가 먼저입니다.
요청 흐름으로 보는 403
403은 한 단계 뒤의 문제입니다. 요청자는 인증되었지만, 리소스가 요구하는 권한과 현재 Authentication의 권한이 맞지 않습니다.
GET /api/admin/users
Authorization: Bearer 일반사용자토큰
1. JWT 필터가 토큰을 검증한다
2. SecurityContext에 Authentication이 들어간다
3. 인가 단계에서 ROLE_ADMIN 필요 조건을 확인한다
4. 현재 사용자는 ROLE_USER만 가지고 있다
5. AccessDeniedHandler가 403 응답을 만든다403을 만났다면 토큰 파싱만 계속 볼 필요는 없습니다. Authentication 안에 어떤 authority가 들어갔는지, `hasRole`과 `hasAuthority`를 혼용하지 않았는지, role prefix가 맞는지를 봐야 합니다.
AuthenticationEntryPoint와 AccessDeniedHandler
Spring Security에서 두 응답을 분리해서 다루려면 EntryPoint와 AccessDeniedHandler의 역할을 알아야 합니다. 이름이 어렵지만, 기준은 단순합니다.
- AuthenticationEntryPoint: 인증이 필요한데 인증되지 않은 요청을 처리한다
- AccessDeniedHandler: 인증은 되었지만 접근이 거부된 요청을 처리한다
- JWT API에서는 둘 다 JSON 응답으로 통일해 프론트엔드가 처리하기 쉽게 만드는 경우가 많다
http.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
})
);JWT 필터에서 자주 생기는 실수
JWT 필터가 모든 실패를 바로 403으로 내려버리면 디버깅이 어려워집니다. 토큰 없음, 토큰 만료, 권한 부족이 모두 같은 응답으로 보이면 클라이언트도 다음 행동을 정하기 어렵습니다.
- 토큰이 없는데도 403이 나온다: 보호 URL과 익명 인증 처리 흐름을 확인한다
- 토큰이 정상인데 401이 나온다: JWT 필터 위치와 SecurityContext 저장 여부를 확인한다
- 관리자 권한인데 403이 나온다: role prefix, authority 이름, DB 권한 매핑을 확인한다
- preflight 요청이 막힌다: CORS와 OPTIONS 허용 여부를 별도로 확인한다
hasRole과 hasAuthority의 차이
실무에서 403을 만드는 흔한 원인은 권한 문자열 불일치입니다. `hasRole(“ADMIN”)`은 내부적으로 `ROLE_ADMIN`을 기대하는 방식으로 이해해야 합니다. 반면 `hasAuthority(“ADMIN”)`는 말 그대로 `ADMIN` authority를 봅니다.
// Authentication authorities = ["ROLE_ADMIN"]
.requestMatchers("/admin/**").hasRole("ADMIN") // 통과 가능
.requestMatchers("/admin/**").hasAuthority("ADMIN") // 실패 가능권한이 분명히 들어간 것 같은데 403이 계속 나온다면 로그에서 Authentication의 authorities를 그대로 출력해 보는 것이 빠릅니다.
디버깅 순서
- 요청 URL이 어떤 SecurityFilterChain에 걸리는지 확인한다
- Authorization 헤더가 실제 서버까지 들어오는지 확인한다
- JWT 필터가 토큰을 읽고 검증하는지 확인한다
- SecurityContext에 Authentication이 들어가는지 확인한다
- Authentication authorities와 설정의 hasRole/hasAuthority 조건을 비교한다
- EntryPoint와 AccessDeniedHandler가 의도한 응답을 만드는지 확인한다
정리
Spring Security 401과 403은 같은 실패가 아닙니다. 401은 인증 정보를 만들지 못한 문제이고, 403은 인증 이후 권한 판단에서 막힌 문제입니다.
Spring Security 설정 흐름은 Spring Security JSESSIONID 확인법, Spring Security JWT와 OAuth2 Resource Server는 어떻게 다를까, OAuth2 소셜 로그인 후 JWT 발급 구조와 함께 보면 좋습니다. 외부 기준은 Spring Security Reference – Exception Handling, Spring Security API – AuthenticationEntryPoint, Spring Security API – AccessDeniedHandler를 확인했습니다.