기본 콘텐츠로 건너뛰기

CORS·JWT 조합으로 발생하는 401/403 원인별 해결 가이드

CORS·JWT 조합으로 발생하는 401/403 원인별 해결 가이드

AI 생성 이미지: CORS·JWT 조합으로 발생하는 401/403 원인별 해결
AI 생성 이미지: CORS·JWT 조합으로 발생하는 401/403 원인별 해결

문제 정의 — CORS와 JWT가 맞닿을 때 401/403이 발생하는 이유

브라우저의 동일출처 정책과 서버의 JWT 인증 흐름이 만나는 지점에서는 주로 세 가지 계층에서 문제가 생깁니다. 첫째, CORS 사전검사(OPTIONS)가 올바르게 처리되지 않으면 요청이 서버에 도달하지 못하고 브라우저가 네트워크·보안 오류로 간주합니다. 설사 서버가 응답을 반환해도 자바스크립트에서 접근할 수 없습니다. 둘째, Authorization 헤더나 쿠키를 보내려면 서버가 Access-Control-Allow-Headers에 Authorization을 포함해야 하고, 자격증명 전송 시에는 Access-Control-Allow-Credentials: true와 명시적 Origin 설정이 필요합니다 — 와일드카드는 쓸 수 없습니다. 셋째, 요청이 서버까지 도달하더라도 JWT가 누락되거나 만료·변조된 경우 401을, 권한이 부족하면 403을 반환합니다. 즉 동일한 클라이언트 오류라도 원인은 프리플라이트(CORS) 설정, 클라이언트 전송 방식, 또는 서버의 인증·권한 검사 중 어느 쪽인지로 나뉩니다. CORS·JWT 조합으로 발생하는 401/403 원인별 해결을 빠르게 진단하려면 아래 실무 체크리스트를 참고하세요: ① 프리플라이트(OPTIONS) 응답 존재 확인 ② Access-Control-Allow-Headers에 Authorization 포함 여부 확인 ③ Allow-Credentials와 Origin 일치 여부 확인 ④ 서버에서 JWT 유효성 및 권한 검증 수행.

  • 표시 단서: 브라우저 콘솔에 표시되는 CORS 오류는 보통 클라이언트·서버 간 설정 문제를 가리킵니다. 반면 네트워크 탭에서 401 또는 403 응답이 보이면 서버의 인증·권한 검증 실패일 가능성이 큽니다.
  • 오류 원인 요약: Access-Control-Allow-Headers에 Authorization 누락, 프리플라이트(OPTIONS) 미응답, Allow-Credentials/Origin 불일치, 그리고 JWT 누락·만료·검증 실패.

브라우저 관점의 CORS 핵심: 기본 규칙, 프리플라이트, 자격증명

브라우저는 동일출처 정책을 기본으로 하며, CORS는 이를 우회하기 위한 약속이다. 핵심은 요청이 '단순 요청'인지와 자격증명(credentials)이 전송되는지 여부다. 이 요약은 CORS·JWT 조합으로 발생하는 401/403 원인별 해결에 실무적으로 도움이 된다.

  • 프리플라이트: 비단순 요청(예: PUT/DELETE, 커스텀 헤더 — Authorization 등)은 먼저 OPTIONS 프리플라이트를 보낸다. 서버는 Access-Control-Allow-Methods와 Access-Control-Allow-Headers로 허용 항목을 응답해야 실제 요청이 전송된다. 또한 응답을 Max-Age로 캐시할 수 있다.
  • 자격증명 영향: fetch/XHR에서 credentials: 'include'를 설정해야 쿠키나 HTTP 인증 헤더가 전송된다. 서버는 Access-Control-Allow-Credentials: true를 반환해야 하며, 이 경우 Access-Control-Allow-Origin에 '*'을 쓸 수 없고 요청한 Origin을 정확히 반환해야 한다.
  • JWT/Authorization 헤더: 요청에 Authorization 헤더를 포함하면 프리플라이트에 Access-Control-Request-Headers: Authorization이 나타난다. 서버는 Access-Control-Allow-Headers에 Authorization을 명시해야 하고, 응답 헤더로 토큰을 전달해 클라이언트에서 읽게 하려면 해당 헤더를 Access-Control-Expose-Headers에 추가해야 한다.
  • 문제 증상: credentials가 빠지면 서버는 토큰이나 쿠키를 받지 못해 401을 반환한다. Access-Control-Allow-Origin/Access-Control-Allow-Credentials 불일치나 프리플라이트 실패가 있으면 브라우저가 요청을 차단해 실제 응답이 보이지 않고 CORS 에러 또는 403으로 처리되는 경우가 있다. 간단 체크리스트: 1) 요청에 credentials 포함 여부 확인, 2) 서버가 요청 Origin을 정확히 반환하는지 검증, 3) 프리플라이트에서 필요한 메서드와 헤더를 허용했는지 점검.

Authorization 헤더와 쿠키: JWT 전달 방식별 문제 및 증상

Authorization 헤더 방식

  • 원인: 클라이언트가 Authorization 헤더를 설정하지 않았거나, CORS에서 Access-Control-Allow-Headers에 해당 헤더를 등록하지 않아 브라우저가 요청 헤더를 차단함
  • 증상: DevTools 네트워크 탭에 Authorization 헤더가 보이지 않음. 서버는 인증 정보를 받지 못해 401을 반환하고, 토큰은 있지만 권한이 부족하면 403을 반환

쿠키 방식

  • 원인: 쿠키의 SameSite(Lax/Strict)·secure 제한, fetch/axios 요청에서 credentials 미지정, 또는 서버에서 Access-Control-Allow-Credentials를 허용하지 않음
  • 증상: 브라우저가 쿠키를 전송하지 않아 서버가 인증 정보를 받지 못해 401이 발생함. 또한 HTTPS 요구 조건을 충족하지 못하면 쿠키가 전송되거나 저장되지 않아 401/403으로 이어질 수 있음

진단 팁: DevTools 네트워크 탭에서 요청 헤더와 쿠키 전송 여부를 확인하고, preflight 응답의 CORS 헤더와 Set-Cookie, Access-Control-Allow-Credentials 설정을 점검하세요. 간단한 체크리스트 예: 1) Authorization 헤더가 포함되었는가? 2) 요청에 credentials가 설정되었는가? 3) 서버가 Access-Control-Allow-Headers와 Access-Control-Allow-Credentials를 올바르게 반환하는가? 이 내용은 CORS·JWT 조합으로 발생하는 401/403 원인별 해결에 도움이 됩니다.

서버·프록시 설정으로 해결하기 — Access-Control과 인증 검증 설정 체크리스트

  • Access-Control-Allow-Origin : CORS·JWT 조합으로 발생하는 401/403을 방지하려면, 인증이 필요한 요청에서 와일드카드(*)를 사용하지 마세요. 클라이언트 Origin을 정확히 반환하거나 동적 화이트리스트를 적용해 허용합니다.
  • Access-Control-Allow-Credentials : 쿠키나 인증 헤더를 사용할 때는 서버에서 true로 설정하고, 클라이언트는 fetch/axios에서 credentials:'include' 또는 withCredentials:true를 지정하세요.
  • Access-Control-Expose-Headers : 서버가 토큰을 응답 헤더로 재발급할 경우 Authorization 등 필요한 헤더를 노출하도록 설정합니다.
  • Access-Control-Allow-Headers : 클라이언트가 보낼 모든 커스텀·인증 헤더를 명시하세요. 예: Authorization, Content-Type, X-Requested-With.
  • Preflight 처리 : OPTIONS 요청은 인증 미들웨어를 우회해 200/204로 신속히 응답하도록 하고, 불필요한 Preflight를 줄이기 위해 합리적인 Access-Control-Max-Age를 설정합니다. 실무 체크리스트: 브라우저 콘솔에서 OPTIONS 응답과 노출된 헤더를 확인해 보세요.
  • 프록시/로드밸런서 : 프록시나 로드밸런서가 Authorization 헤더와 쿠키를 올바르게 전달하는지 설정을 점검하세요. 예: NGINX의 proxy_set_header Authorization $http_authorization.
  • 미들웨어 순서 : CORS 관련 미들웨어를 인증 검사보다 먼저 실행하도록 배치합니다. 실제 인증 로직은 OPTIONS가 아닌 실제 요청 메서드에서 수행해야 합니다.
  • 토큰 위치 일치 : 토큰을 Bearer 헤더로 보낼지 쿠키로 보낼지 서버의 검증 로직과 정책을 일관되게 맞추세요.

JWT 자체 문제로 인한 401/403 원인과 대응(만료·서명·클레임 불일치)

만료, 서명 불일치, 클레임 불일치로 발생하는 401/403을 빠르게 진단하고 안전하게 처리하는 실무 지침입니다. 특히 CORS·JWT 조합으로 발생하는 401/403 원인별 해결 상황도 함께 고려하세요.

  • 만료(exp)·리프레시 전략: 액세스 토큰은 짧게(예: 5–15분) 발급하고, 리프레시 토큰은 httpOnly·Secure 옵션으로 안전하게 보관합니다. 액세스 토큰 만료 시 서버는 401을 반환하고 클라이언트는 리프레시 엔드포인트에 재발급을 요청하도록 설계하세요. 리프레시 토큰 회전(one‑time use)과 폐기 목록(revocation list)을 도입해 탈취 리스크를 줄입니다. 체크리스트 예: ① 액세스/리프레시 TTL 정의 ② 리프레시 토큰 회전 적용 ③ 토큰 폐기·감사 로그 활성화.
  • 시계 차이(Clock skew): exp/nbf 검증에 30–120초의 여유를 둡니다. 인증 서버와 서비스는 NTP로 동기화하고, 로컬 검증 시 허용 범위를 문서화해 운영자가 쉽게 확인할 수 있게 하세요.
  • 서명/키 불일치: alg 고정 검증(‘none’ 금지)을 실시하고, kid를 사용해 JWKS에서 공개키를 조회한 뒤 서명을 검증합니다. 서명 검증 실패는 401로 처리하며, 키 로테이션 시 캐시 만료와 롤링 키 전략을 적용해 다운타임과 오류를 최소화합니다.
  • 클레임 불일치(권한): 토큰 자체는 유효하지만 권한이 부족한 경우에는 403을 반환합니다(예: aud/iss/roles 검증 실패). 권한 검증 정책은 중앙화하고, 권한 실패 사유는 내부 로그에 남기되 응답에는 민감 정보를 노출하지 마세요.
  • 오류 코드·재발급 흐름 설계: 인증 실패는 401, 권한 실패는 403으로 명확히 구분합니다. 재발급 엔드포인트는 리프레시 토큰의 유효성·회전 상태를 확인한 뒤 새 액세스 토큰(필요 시 새 리프레시 토큰)을 발급해야 합니다. 실패 원인은 내부 로그와 모니터링에 남겨 문제를 추적할 수 있게 하세요.

실전 진단·로깅·방지책 — 체크리스트와 모범 사례

  • 브라우저 네트워크 탭
    • 프리플라이트(OPTIONS) 응답의 Access-Control-* 헤더(Allow-Origin, Allow-Credentials, Allow-Methods, Allow-Headers)를 꼼꼼히 확인하세요.
    • 실제 요청에 Authorization 헤더가 포함되는지, 그리고 withCredentials 설정이 올바른지 확인합니다. 예: curl로 OPTIONS와 GET을 각각 호출해 헤더를 비교해 보세요.
    • 응답의 HTTP 상태 코드와 본문(401 vs 403)으로 원인을 구분합니다. 상태와 메시지를 함께 봐야 정확한 진단이 가능합니다.
  • 서버 로그 분석
    • 로그에 요청 origin, 요청 ID, Authorization 헤더 유무와 JWT 검증 오류 종류(만료, 서명 불일치 등)를 남기세요.
    • CORS 미들웨어가 인증 로직보다 먼저 실행되는지 확인해 의도치 않은 차단이 발생하지 않게 합니다.
  • 자동화된 테스트
    • CI 파이프라인에서 프리플라이트와 자격증명 포함/미포함 케이스를 E2E로 검증하세요. 실패 시 재현 가능한 테스트가 있어야 합니다.
    • 미들웨어 단위 테스트로 CORS 헤더와 인증 흐름을 분리해 검증합니다. 경계 조건에 대한 단위 테스트를 권장합니다.
  • 권장 구성(정책·보안·UX)
    • 허용 Origin은 화이트리스트로 명시하고 Allow-Credentials 사용은 최소화하세요.
    • JWT는 수명을 짧게 설정하고 리프레시 토큰을 활용합니다. 쿠키를 쓸 경우 SameSite와 Secure 옵션을 반드시 설정하세요.
    • 인증 관련 헤더를 Access-Control-Expose-Headers에 포함시키고, 프런트에서 처리하기 쉬운 일관된 401/403 메시지를 제공하면 대응이 수월합니다.
    • 사용자에게 재로그인이나 권한 부족 시 행동 지침을 명확히 보여주고, 재시도 방법을 안내하세요.

경험에서 배운 점

브라우저의 CORS 정책과 서버의 JWT 인증은 서로 다른 층에서 동작합니다. CORS는 클라이언트(브라우저)가 차단하는 문제이고 401/403은 서버가 반환하는 상태이므로, 브라우저에서 보이는 401/403이 항상 JWT 자체의 문제인 것은 아닙니다. 예컨대 Authorization 헤더가 누락되거나 프리플라이트(OPTIONS) 요청이 인증 때문에 차단되면 브라우저가 실제 요청을 보내지 못해 결과적으로 401/403처럼 보일 수 있습니다. 따라서 프리플라이트는 인증 없이 허용하고, 항상 적절한 Access-Control-* 헤더를 포함시키는 것이 우선입니다. 한마디로, CORS와 인증을 분리해 진단하는 습관이 중요합니다(참고: CORS·JWT 조합으로 발생하는 401/403 원인별 해결 관점).

실무에서 자주 발생하는 실수는 대체로 다음과 같습니다. (1) credentialed 요청인데 Access-Control-Allow-Origin을 "*"로 두거나 Allow-Credentials를 빼먹는 경우, (2) 인증 실패나 오류 응답에 CORS 헤더를 포함하지 않아 브라우저가 실제 원인을 가려버리는 경우, (3) 리버스 프록시나 API 게이트웨이가 Authorization 헤더나 쿠키를 제거·변경하는 경우, (4) JWT 만료나 시계 오프셋(clock skew)을 고려하지 않아 간헐적으로 401이 발생하는 경우. 재발 방지를 위해 프리플라이트와 실제 요청을 분리해 테스트(브라우저 DevTools의 Network 탭과 curl 비교)하고, 오류 응답에도 일관된 CORS 헤더를 붙이는 표준화를 도입하세요.

• 프리플라이트(OPTIONS)는 인증 없이 200/204로 응답하도록 설정하고 항상 Access-Control-Allow-Methods/Headers를 반환할 것
• credentialed 요청이면 Access-Control-Allow-Credentials: true를 설정하고, 이때 Origin은 반드시 정확한 출처(origin)로 반환(“*” 금지)
• 성공/오류/인증실패 등 모든 응답에 일관된 CORS 헤더를 포함시켜 브라우저가 원인을 판단할 수 있게 할 것
• Authorization 헤더를 사용하는 경우 Access-Control-Allow-Headers에 Authorization을 추가하고, 프록시가 해당 헤더를 유지하는지 검증할 것
• 쿠키를 사용할 때는 Secure, HttpOnly, SameSite=None을 명확히 설정하고 HTTPS에서만 동작하도록 할 것
• JWT는 만료(time)와 iss/aud 같은 발급 정보를 로그에 남기고, 시계 오프셋을 허용해 간헐적 401을 완화할 것
• 문제 재현 시 브라우저와 curl(httpie) 결과를 비교: curl이 통과하고 브라우저가 실패하면 CORS일 가능성이 높음
• 감사·모니터링: 401/403 발생 시 “프리플라이트 차단인지 / 서버 인증 실패인지 / 프록시 문제인지” 구분할 수 있는 진단 로그와 가이드를 준비할 것
• 실무 체크리스트 예: 프리플라이트 응답 확인 → CORS 헤더 일관성 확인 → 프록시 헤더 보존 확인 → JWT 만료/시계차 검증

댓글

이 블로그의 인기 게시물

Java Servlet Request Parameter 완전 정복 — GET/POST 모든 파라미터 확인 & 디버깅 예제 (Request Parameter 전체보기)

Java Servlet Request Parameter 완전 정복 — GET/POST 모든 파라미터 확인 & 디버깅 예제 Java Servlet Request Parameter 완전 정복 웹 애플리케이션에서 클라이언트로부터 전달되는 Request Parameter 를 확인하는 것은 필수입니다. 이 글에서는 Java Servlet 과 JSP 에서 GET/POST 요청 파라미터를 전체 출력하고 디버깅하는 방법을 다양한 예제와 함께 소개합니다. 1. 기본 예제: getParameterNames() 사용 Enumeration<String> params = request.getParameterNames(); System.out.println("----------------------------"); while (params.hasMoreElements()){ String name = params.nextElement(); System.out.println(name + " : " + request.getParameter(name)); } System.out.println("----------------------------"); 위 코드는 요청에 포함된 모든 파라미터 이름과 값을 출력하는 기본 방법입니다. 2. HTML Form과 연동 예제 <form action="CheckParamsServlet" method="post"> 이름: <input type="text" name="username"><br> 이메일: <input type="email" name="email"><b...

PostgreSQL 달력(일별,월별)

SQL 팁: GENERATE_SERIES로 일별, 월별 날짜 목록 만들기 SQL 팁: GENERATE_SERIES 로 일별, 월별 날짜 목록 만들기 데이터베이스에서 통계 리포트를 작성하거나 비어있는 날짜 데이터를 채워야 할 때, 특정 기간의 날짜 목록이 필요할 수 있습니다. PostgreSQL과 같은 데이터베이스에서는 GENERATE_SERIES 함수를 사용하여 이 작업을 매우 간단하게 처리할 수 있습니다. 1. 🗓️ 일별 날짜 목록 생성하기 2020년 1월 1일부터 12월 31일까지의 모든 날짜를 '1 day' 간격으로 생성하는 쿼리입니다. WITH date_series AS ( SELECT DATE(GENERATE_SERIES( TO_DATE('2020-01-01', 'YYYY-MM-DD'), TO_DATE('2020-12-31', 'YYYY-MM-DD'), '1 day' )) AS DATE ) SELECT DATE FROM date_series 이 쿼리는 WITH 절(CTE)을 사용하여 date_series 라는 임시 테이블을 만들고, GENERATE_SERIES 함수로 날짜를 채웁니다. 결과 (일별 출력) 2. 📅 월별 날짜 목록 생성하기 동일한 원리로, 간격을 '1 MONTH' 로 변경하면 월별 목록을 생성할 수 있습니다. TO...

CSS로 레이어 팝업 화면 가운데 정렬하는 방법 (top·left·transform 완전 정리)

레이어 팝업 센터 정렬, 이 코드만 알면 끝 (CSS 예제 포함) 이벤트 배너나 공지사항을 띄울 때 레이어 팝업(center 정렬) 을 깔끔하게 잡는 게 생각보다 어렵습니다. 화면 크기가 변해도 가운데에 고정되고, 모바일에서도 자연스럽게 보이게 하려면 position , top , left , transform 을 정확하게 이해해야 합니다. 이 글에서는 아래 내용을 예제로 정리합니다. 레이어 팝업(center 정렬)의 기본 개념 자주 사용하는 position: absolute / fixed 정렬 방식 질문에서 주신 스타일 top: 3.25%; left: 50%; transform: translateX(-50%) 의 의미 실무에서 바로 쓰는 반응형 레이어 팝업 HTML/CSS 예제 1. 레이어 팝업(center 정렬)이란? 레이어 팝업(레이어 팝업창) 은 새 창을 띄우는 것이 아니라, 현재 페이지 위에 div 레이어를 띄워서 공지사항, 광고, 이벤트 등을 보여주는 방식을 말합니다. 검색엔진(SEO) 입장에서도 같은 페이지 안에 HTML이 존재 하기 때문에 팝업 안의 텍스트도 정상적으로 인덱싱될 수 있습니다. 즉, “레이어 팝업 센터 정렬”, “레이어 팝업 만드는 방법”과 같이 관련 키워드를 적절히 넣어주면 검색 노출에 도움이 됩니다. 2. 질문에서 주신 레이어 팝업 스타일 분석 질문에서 주신 스타일은 다음과 같습니다. <div class="layer-popup" style="width:1210px; z-index:9001; position:absolute; top:3.25%; left:50%; transform:translateX(-50%);"> 레이어 팝업 내용 <...