본문 바로가기
개발/ERROR 모음

Spring Security JWT 401/403 오류 해결 가이드 (필터 순서 포함)

by chansungs 2026. 3. 2.
728x90
반응형

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 (체크리스트)

아래가 실무에서 제일 자주 터지는 순서입니다.

  1. Authorization 헤더 자체가 없음
  2. Bearer 접두어 누락/오타
  3. 토큰 만료(exp)
  4. 서명키(secret) 불일치(환경별 설정 차이)
  5. 토큰 파싱/검증 중 예외 발생했는데 필터가 그냥 넘김
  6. Security 설정에서 permitAll/authorizeHttpRequests 설정 실수
  7. Role/권한(GrantedAuthorities) 매핑 실수 → 403
  8. CORS/Preflight(OPTIONS) 요청이 막혀서 401/403

3) 401 Unauthorized 해결: 토큰 누락/만료/서명 오류

(1) Authorization 헤더 확인

클라이언트 요청에 아래 헤더가 있어야 합니다.

 
Authorization: Bearer {JWT}

❌ 흔한 실수

  • 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 허용도 꼭 확인
728x90
반응형