다플 생활 가이드

정책·신청 안내

JWT 인증 실패와 권한 오류 처리 전략: 401과 403 예외 응답 설계 방법

웹 애플리케이션이나 백엔드 API 서버를 구축할 때, 인증(Authentication)과 인가(Authorization)는 보안의 핵심 요소입니다. 최근 REST API 환경에서는 토큰 기반 인증 방식인 JWT(JSON Web Token)를 널리 활용하고 있습니다. 그러나 시스템을 개발하다 보면 토큰 만료, 서명 불일치, 위변조, 접근 권한 부족 등 다양한 오류 상황을 만나게 됩니다.

사용자가 "jwt 자격 증명에 실패하였습니다"라는 문구를 보거나 시스템에서 권한 에러가 발생했을 때, 서버가 명확하고 정교한 에러 응답을 전달하지 않으면 프론트엔드 개발자는 물론 최종 사용자도 문제를 파악하기 어려워집니다. 이번 글에서는 JWT 인증 실패 및 권한 오류가 발생하는 원인을 살펴보고, 이를 안정적으로 처리하기 위한 백엔드 설계 전략에 대해 알아보겠습니다.

01. 핵심 기준 한눈에 보기

JWT 기반 보안 처리 시 가장 기본이 되는 요소는 인증 오류와 권한 오류를 명확히 구분하는 것입니다.

웹 보안 라이프사이클에서 인증과 인가는 엄연히 다른 단계이며, 반환해야 하는 HTTP 상태 코드와 예외 처리 방식도 차이가 있습니다. 두 개념의 차이를 정확히 이해하면 서버 응답 구조를 더욱 직관적으로 설계할 수 있습니다.

구분인증 실패 (Authentication Failure)권한 오류 (Authorization Failure)
의미"당신이 누구인지 증명할 수 없습니다.""당신이 누군지는 알지만, 이 자원에 접근할 권한이 없습니다."
HTTP 상태 코드401 Unauthorized403 Forbidden
주요 원인토큰 누락, 토큰 만료, 서명 위변조, 잘못된 형식일반 사용자가 관리자 페이지 접근, 타인 리소스 접근
처리 프레임워크AuthenticationEntryPointAccessDeniedHandler
클라이언트 대응재로그인 요청 또는 Refresh Token을 통한 재발급권한 부족 안내 페이지 이동 또는 접근 금지 알림
광고

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

02. JWT 인증 실패와 권한 오류의 개념 이해

인증 실패와 권한 오류는 비슷해 보이지만 서비스 로직상 완전히 다른 시점에서 발생합니다.

✓ 인증 실패(401 Unauthorized)란?

인증 실패는 요청자가 올바른 자격 증명을 제공하지 못했을 때 일어납니다. JWT 환경에서는 다음과 같은 상황이 대표적입니다.

  • HTTP 요청 헤더(Authorization: Bearer <token>)에 토큰이 누락된 경우

  • 토큰의 유효 기간(Expiration Time)이 만료된 경우

  • 토큰의 시크릿 키가 맞아떨어지지 않아 서명 검증에 실패한 경우

  • 토큰의 구조나 형식이 유효하지 않은 경우

이러한 경우 서버는 401 Unauthorized 응답을 반환하여 사용자가 신원을 다시 증명하도록 유도해야 합니다.

✓ 권한 오류(403 Forbidden)란?

권한 오류는 사용자의 신원은 확인되었으나 해당 요청을 수행할 수 있는 자격이 없는 상황입니다.

  • 로그인된 일반 회원이 관리자 전용 API(/api/v1/admin/*)를 호출하는 경우

  • 특정 게시글의 작성자가 아닌 다른 사용자가 수정/삭제 API를 호출하는 경우

이 상황에서는 재로그인을 하더라도 권한 자체가 없는 것이므로, 서버는 403 Forbidden 응답을 반환하여 접근이 금지되었음을 안내해야 합니다.

광고

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

03. Spring Security에서의 JWT 예외 처리 전략

Spring Security 환경에서 JWT 인증과 권한을 처리할 때 주의해야 할 점은 서블릿 필터(Filter) 단의 예외입니다.

Spring의 @RestControllerAdvice@ExceptionHandler는 Spring MVC Controller 레이어 내부에서 발생한 예외만 수집합니다. 하지만 JWT 검증은 Controller에 도달하기 전인 Security Filter Chain 단계에서 진행되므로, 필터 내부에서 발생한 예외는 일반적인 예외 핸들러로 잡히지 않습니다. 따라서 Spring Security가 제공하는 전용 인터페이스를 구현해야 합니다.

✓ AuthenticationEntryPoint 구현 (401 처리)

토큰 검증에 실패하거나 자격 증명이 없는 상태로 보호된 리소스에 접근할 때 작동합니다. AuthenticationEntryPoint 인터페이스를 구현하여 커스텀 JSON 응답을 내려주도록 설정합니다.

Java
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, 
                         HttpServletResponse response, 
                         AuthenticationException authException) throws IOException {
        
        // 필터에서 전달받은 예외 속성 확인
        String exception = (String) request.getAttribute("exception");
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        // 예외 세부 유형에 따른 응답 메시지 구성
        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("status", 401);
        errorDetails.put("error", "UNAUTHORIZED");
        
        if ("EXPIRED_TOKEN".equals(exception)) {
            errorDetails.put("code", "AUTH_001");
            errorDetails.put("message", "토큰이 만료되었습니다.");
        } else {
            errorDetails.put("code", "AUTH_002");
            errorDetails.put("message", "jwt 자격 증명에 실패하였습니다.");
        }
        
        response.getWriter().write(objectMapper.writeValueAsString(errorDetails));
    }
}

✓ AccessDeniedHandler 구현 (403 처리)

인증은 완료되었으나 요청한 리소스의 Role(권한)이 부족할 때 작동합니다. AccessDeniedHandler 인터페이스를 구현합니다.

Java
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, 
                       HttpServletResponse response, 
                       AccessDeniedException accessDeniedException) throws IOException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        
        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("status", 403);
        errorDetails.put("error", "FORBIDDEN");
        errorDetails.put("code", "AUTH_003");
        errorDetails.put("message", "해당 리소스에 접근할 권한이 없습니다.");
        
        response.getWriter().write(objectMapper.writeValueAsString(errorDetails));
    }
}

04. 클라이언트(프론트엔드) 대처 및 토큰 재발급 흐름

서버에서 명확한 에러 코드와 상태 코드를 전달하면, 프론트엔드 애플리케이션에서도 유연하게 대응할 수 있습니다.

  1. 토큰 만료 예외(401 & 특정 에러 코드) 수신

    • 프론트엔드의 Interceptor(예: Axios Interceptor)가 401 응답을 감지합니다.

  2. Refresh Token을 통한 Access Token 재발급 요청

    • 기존 요청을 잠시 대기시키고, 저장소에 보관된 Refresh Token을 서버로 보내 새로운 Access Token을 발급받습니다.

  3. 원래 요청 재시도(Retry)

    • 새로 발급받은 Access Token을 헤더에 재설정하고, 이전에 실패했던 API 요청을 다시 수행합니다.

  4. 재발급 실패 또는 권한 오류(403) 발생 시

    • Refresh Token마저 만료되었다면 저장된 토큰을 삭제하고 로그인 화면으로 이동시킵니다.

    • 403 오류의 경우 "접근 권한이 없습니다"라는 안내 메시지를 출력하고 이전 페이지로 되돌립니다.

FAQ

Q1. 401 오류와 403 오류를 구분하지 않고 400이나 500으로 통일해서 응답해도 되나요?

권장하지 않습니다. HTTP 상태 코드는 API를 사용하는 클라이언트와의 표준 약속입니다. 401은 로그인이나 토큰 재발급이 필요하다는 의미를 전달하고, 403은 접근 자체가 불가능하다는 의미를 명확히 구분해주므로 표준 가이드라인인 401과 403을 구분해 사용하는 것이 올바른 처리 전략입니다.

Q2. "jwt 자격 증명에 실패하였습니다" 오류가 지속적으로 발생하는 이유는 무엇인가요?

해당 오류는 주로 요청 헤더의 Authorization 값이 올바른 Bearer 포맷을 갖추지 않았거나, 토큰 서명에 사용된 시크릿 키가 서버의 설정값과 일치하지 않을 때 발생합니다. 클라이언트에서 토큰을 보낼 때 앞에 Bearer (공백 포함) 접두사를 정확히 붙였는지 확인해 보실 필요가 있습니다.

Q3. JWT 만료 시간은 어느 정도로 설정하는 것이 좋은가요?

보안성을 높이기 위해서는 Access Token의 유효 기간을 15분~1시간 정도로 짧게 설정하는 것이 좋습니다. 그리고 상대적으로 긴 유효 기간(1주~2주)을 가질 수 있는 Refresh Token을 별도로 운용하여, 보안성과 사용자 편의성을 동시에 확보하는 구성을 추천해 드립니다.

광고

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

핵심키워드: Spring Security 기반 JWT 토큰 재발급(Refresh Token) 완벽 구현 가이드

관련 키워드

#JWT 인증 실패#jwt 오류#jwt 권한 설정#jwt 자격 증명에 실패하였습니다#jwt 권한 처리#Spring Security JWT#401 Unauthorized#403 Forbidden
목록