[Java/Spring Boot] Spring Boot에서 OAuth2 인증 401 오류 해결하기

2025. 9. 14. 18:55·개발 (Development)/Java

Spring Boot 기반 서비스에서 OAuth2 인증을 적용하다 보면, 어떤 API는 잘 동작하는데 특정 API에서만 401 Unauthorized 오류가 발생하는 경우가 있습니다. 이번 글에서는 실제 문제 해결 과정을 바탕으로, 원인 파악과 수정 방법을 정리해 보겠습니다.

문제 상황

  • 여러 개의 API 서비스를 운영하며 공통 인증을 적용했습니다.
  • 일부 API는 정상 응답하지만, 특정 API 호출 시 401 Unauthorized 오류가 발생했습니다.
  • 서버 로그에는 별다른 오류가 없어 원인 추적이 쉽지 않았습니다.

원인 후보

처음에는 다양한 가능성을 의심했습니다.

  1. 보안 필터 체인 매칭 문제: 특정 경로가 올바른 SecurityFilterChain을 타지 않는 경우
  2. 게이트웨이/프록시 문제: Authorization 헤더가 일부 경로에서 전달되지 않는 경우
  3. 쿠키 기반 인증 문제: 쿠키 설정(SameSite/도메인)으로 인해 토큰이 전달되지 않는 경우
  4. CORS 문제: OPTIONS 요청이 차단되어 본 요청까지 가지 못하는 경우
  5. 토큰 파싱 문제: Authorization 헤더에서 “Bearer ” 접두어를 제거하지 않아 인증 실패가 발생하는 경우

실제 코드 점검

기존 커스텀 토큰 추출기

@Component
public class CustomBearerTokenExtractor implements TokenExtractor {
    @Override
    public Authentication extract(HttpServletRequest request) {
        String tokenValue = request.getHeader("Authorization");

        if (StringUtils.isEmpty(tokenValue)) {
            Cookie token = WebUtils.getCookie(request, "LOGIN_TOKEN");
            if (token != null) {
                tokenValue = token.getValue();
            }
        }

        return new PreAuthenticatedAuthenticationToken(tokenValue, "N/A");
    }
}

문제점

  • Authorization: Bearer <토큰> 전체 문자열을 그대로 사용 → “Bearer ” 접두어 제거 누락
  • 토큰이 없더라도 Authentication 객체를 생성 → 불필요한 예외 및 401 유발

해결 방법

수정된 커스텀 토큰 추출기

@Component
public class CustomBearerTokenExtractor implements TokenExtractor {

    private static final String BEARER_PREFIX = "Bearer ";

    @Override
    public Authentication extract(HttpServletRequest request) {
        String token = resolveFromHeader(request);
        if (token == null) token = resolveFromCookie(request, "LOGIN_TOKEN");
        if (token == null) return null; // 토큰 없으면 null 반환

        return new PreAuthenticatedAuthenticationToken(token, "N/A");
    }

    private String resolveFromHeader(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith(BEARER_PREFIX)) {
            return header.substring(BEARER_PREFIX.length()).trim();
        }
        return null;
    }

    private String resolveFromCookie(HttpServletRequest request, String name) {
        Cookie c = WebUtils.getCookie(request, name);
        return (c != null && !c.getValue().isBlank()) ? c.getValue().trim() : null;
    }
}

개선 사항

  • Bearer 접두어를 제거하고 순수 토큰만 인증 모듈로 전달
  • 토큰이 없으면 null 반환 → Spring Security가 올바르게 401 처리

결과

  • 프론트엔드에서 Authorization 헤더를 포함해 호출하면 인증이 정상적으로 처리됩니다.
  • 401 Unauthorized 오류가 더 이상 발생하지 않았습니다.
  • 인증 구조가 명확해지고 유지보수도 쉬워졌습니다.

배운 점

  1. 표준 포맷 준수: OAuth2 Bearer 토큰은 반드시 “Bearer ” 접두어를 제거하고 처리해야 합니다.
  2. null 반환 원칙: 토큰이 없을 때는 Authentication 객체를 억지로 만들지 않고 null을 반환해야 합니다.
  3. 환경 전반 점검 필요: 헤더 전달, 게이트웨이 설정, CORS, 쿠키 정책 등도 인증 실패의 주요 원인이 될 수 있습니다.
  4. 최신 방식 고려: Spring Security 최신 버전에서는 BearerTokenResolver 사용을 권장하므로 점진적 마이그레이션을 고려하는 것이 좋습니다.

결론

이번 문제는 단순히 “Bearer 접두어 제거 누락” 때문에 발생한 오류였습니다. 하지만 원인을 추적하는 과정에서 인증 체계를 전반적으로 점검하고 개선할 수 있었고, 결과적으로 더 안정적인 인증 환경을 마련할 수 있었습니다.

반응형

'개발 (Development) > Java' 카테고리의 다른 글

[Java/Spring Boot] "No thread-bound request found" 에러 원인과 해결법  (0) 2025.09.19
[Java/Spring Boot] JdbcTemplate으로 여러 데이터베이스 연결하기  (0) 2025.09.14
[Java] JPA에서 DTO는 Interface로 구현할까? Class로 구현할까?  (4) 2025.08.03
[Java] Interface란? 클래스와 다른 점은?  (0) 2025.08.03
[Java] MyBatis와 JPA 속도 비교: 어떤 상황에 어떤 선택이 더 나을까?  (0) 2025.08.03
'개발 (Development)/Java' 카테고리의 다른 글
  • [Java/Spring Boot] "No thread-bound request found" 에러 원인과 해결법
  • [Java/Spring Boot] JdbcTemplate으로 여러 데이터베이스 연결하기
  • [Java] JPA에서 DTO는 Interface로 구현할까? Class로 구현할까?
  • [Java] Interface란? 클래스와 다른 점은?
LoopThinker
LoopThinker
모르는 것을 알아가고, 아는 것을 더 깊게 파고드는 공간
  • LoopThinker
    CodeMemoir
    LoopThinker
  • 전체
    오늘
    어제
    • 분류 전체보기 (231)
      • 개발 (Development) (165)
        • Algorithm (1)
        • Angular (1)
        • AWS (6)
        • DeepSeek (2)
        • Docker (7)
        • Git (3)
        • Java (34)
        • JavaScript (4)
        • Kafka (5)
        • Kubernetes (4)
        • Linux (7)
        • PostgreSQL (38)
        • Python (31)
        • React (3)
        • TypeScript (3)
        • Vue.js (5)
        • General (11)
      • 데이터 분석 (Data Analysis) (1)
      • 알고리즘 문제 풀이 (Problem Solving.. (27)
      • 자격증 (Certifications) (24)
        • ADsP (14)
        • 정보처리기사 (4)
        • Linux Master (5)
        • SQLD (1)
      • 기술 동향 (Tech Trends) (11)
      • 기타 (Others) (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Spring boot
    ADsP
    오답노트
    Kafka
    timescaledb
    python
    deepseek
    springboot
    pandas
    자바
    Vue.js
    백준자바
    백준알고리즘
    데이터분석
    Linux master
    Kubernetes
    JSON
    백준온라인저지
    Linux
    java
    MyBatis
    AWS
    docker
    JPA
    리눅스 마스터 2급 2차
    백준
    javascript
    리눅스 마스터 2급
    DevOps
    PostgreSQL
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
LoopThinker
[Java/Spring Boot] Spring Boot에서 OAuth2 인증 401 오류 해결하기
상단으로

티스토리툴바