Spring Security JWT 인증에서 401/403 오류가 발생하는 원인과 해결 방법을 정리했습니다.
401과 403의 차이, Authorization Bearer 토큰 누락/형식 오류, CORS/Preflight, SecurityFilterChain 설정, JWT 필터 추가 위치(필터 순서), AuthenticationEntryPoint/AccessDeniedHandler 처리까지 실무 체크리스트로 해결하세요.

Spring Security JWT 인증 401/403 오류 해결 (필터 순서 포함)
JWT 기반 인증을 붙이면 거의 반드시 한 번은 마주치는 문제가 있습니다.
- 401 Unauthorized
- 403 Forbidden
토큰을 넣었는데도 401이 뜨거나, 로그인은 된 것 같은데 403이 뜨는 경우가 많습니다.
이 글에서는 401/403 차이부터 원인별 해결, 그리고 가장 중요하게 많이 실수하는 JWT 필터 순서까지 정리합니다.
1) 401 vs 403 차이부터 정리
둘은 의미가 완전히 다릅니다.
- 401 Unauthorized
→ “인증(Authentication) 실패”
→ 즉, 누구인지 확인이 안 됨 (토큰 없음/만료/검증 실패) - 403 Forbidden
→ “인가(Authorization) 실패”
→ 즉, 누군지는 아는데 권한이 없음 (ROLE 부족, 접근 제한)
📌 빠른 판단법
- 토큰이 없거나 토큰 검증이 실패하면 → 401
- 토큰은 유효한데 권한이 부족하면 → 403
2) 가장 흔한 원인 TOP 8 (체크리스트)
아래가 실무에서 제일 자주 터지는 순서입니다.
- Authorization 헤더 자체가 없음
- Bearer 접두어 누락/오타
- 토큰 만료(exp)
- 서명키(secret) 불일치(환경별 설정 차이)
- 토큰 파싱/검증 중 예외 발생했는데 필터가 그냥 넘김
- Security 설정에서 permitAll/authorizeHttpRequests 설정 실수
- Role/권한(GrantedAuthorities) 매핑 실수 → 403
- CORS/Preflight(OPTIONS) 요청이 막혀서 401/403
3) 401 Unauthorized 해결: 토큰 누락/만료/서명 오류
(1) Authorization 헤더 확인
클라이언트 요청에 아래 헤더가 있어야 합니다.
❌ 흔한 실수
- Authorization: {JWT} (Bearer 없음)
- Authorizaiton (헤더 키 오타)
- 공백: Bearer{JWT} (띄어쓰기 없음)
(2) 토큰 만료(exp) 확인
토큰에 exp가 걸려 있으면 시간이 지나면 무조건 401이 뜹니다.
✅ 해결
- 만료 시간을 늘리거나
- refresh token 재발급 로직 적용
(3) secret 키/서명 알고리즘 불일치 (dev/prod)
로컬에서는 되는데 운영에서만 401이면 이 케이스가 많습니다.
✅ 체크
- dev/prod의 JWT_SECRET이 같은지
- base64 인코딩 여부가 같은지
- HS256/RS256 혼용 여부
4) 403 Forbidden 해결: 권한(Role) / 인가 문제
토큰이 유효한데도 403이면 보통 아래 중 하나입니다.
(1) 권한이 없는 URL에 접근
예:
.requestMatchers("/admin/**").hasRole("ADMIN")
근데 토큰에 ROLE_ADMIN이 없으면 403입니다.
(2) ROLE prefix 문제
스프링 시큐리티는 기본적으로 Role에 ROLE_ prefix를 기대합니다.
- 권한이 ADMIN으로 들어갔는데
- 시큐리티는 ROLE_ADMIN을 기대하면
→ 매칭이 안 돼서 403
✅ 해결
- 권한 저장을 ROLE_ADMIN으로 맞추거나
- hasAuthority("ADMIN") 등으로 통일
5) JWT 필터 순서: 어디에 끼워야 하나? (핵심)
JWT 인증은 보통 UsernamePasswordAuthenticationFilter 이전에 토큰을 읽어서
SecurityContext에 Authentication을 넣어줘야 합니다.
✅ 가장 흔한 정답:
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
왜 before가 중요할까?
- UsernamePasswordAuthenticationFilter는 “폼 로그인” 쪽 필터라서
JWT 방식에서는 그 이전에 인증 정보를 세팅해줘야 함 - 늦게 넣으면 이미 “인증 없음”으로 판단되어 401/403로 떨어질 수 있음
6) CORS/Preflight 때문에 401/403 나는 케이스
프론트에서 API 호출 시 브라우저는 먼저 OPTIONS preflight를 날릴 수 있습니다.
이 OPTIONS가 security에서 막히면 실제 요청이 가기도 전에 401/403이 발생합니다.
✅ 해결 방법
- OPTIONS는 허용하거나
- CORS 설정을 SecurityFilterChain에 명시
예:
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
7) Spring Security 설정 예시 (SecurityFilterChain)
(스프링 시큐리티 최신 스타일 기준)
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(eh -> eh
.authenticationEntryPoint(customEntryPoint) // 401 처리
.accessDeniedHandler(customDeniedHandler) // 403 처리
)
.build();
}
📌 핵심 3줄
- STATELESS
- anyRequest().authenticated()
- addFilterBefore(...)
8) 디버깅 팁: 어디서 막히는지 추적
401/403 해결은 “토큰이 필터를 타는지”부터 확인하면 빨라집니다.
✅ JWT 필터에서 로그 찍기
- Authorization 헤더가 들어오는지
- 토큰 파싱/검증이 성공하는지
- SecurityContext에 Authentication이 세팅되는지
✅ 실패 시:
- 토큰 검증 실패 → 401로 보내기 (EntryPoint)
- 권한 부족 → 403로 보내기 (DeniedHandler)
✅ 결론
- 401: 인증 실패 (토큰 없음/만료/서명 불일치/필터에서 인증 세팅 실패)
- 403: 인가 실패 (권한 매핑/ROLE prefix/URL 권한 설정 문제)
- JWT 필터는 보통 UsernamePasswordAuthenticationFilter 이전이 안전하다.
- 프론트 연동이면 CORS/OPTIONS 허용도 꼭 확인
'개발 > ERROR 모음' 카테고리의 다른 글
| Spring @Transactional 적용 안 되는 이유 총정리 (AOP 프록시, private, self 호출) (0) | 2026.03.16 |
|---|---|
| Cannot load JDBC driver class 해결 방법 (원인 7가지 체크리스트) (0) | 2026.03.09 |
| MyBatis Invalid bound statement (not found) 해결 방법 (원인 7가지 체크리스트) (0) | 2026.02.24 |
| Spring Boot Whitelabel Error Page 해결 가이드 (404/500 원인별 정리) (0) | 2026.02.17 |
| Spring BeanCreationException 원인 TOP 5 (실무 해결 체크리스트) (0) | 2026.02.10 |