본문 바로가기
Server

[Server] CORS ERROR를 피하는 방법 - 교차 출처 리소스 공유(CORS)

by 마진 2025. 9. 21.

 

CORS란?

CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 브라우저가 서로 다른 출처(Origin) 간의 리소스 공유를 안전하게 허용하기 위해 사용하는 HTTP 헤더 기반 보안 메커니즘이다. 

 

교차 출처 요청은 다른 서버에서 응답받은 리소스를 기존 웹페이지에서 사용할 때 발생하는데, 도메인 A (https://domain-a.com)의 웹페이지에서 script를 통해 도메인 B의 이미지(https://domain-b.com/images/sample.jpg)를 요청해서 웹페이지에 공유하는 경우가 간단한 예시다.

 

보안상의 이유로 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한하기 때문에 javascript의 fetch() 메서드와 XMLHttpRequest는 '동일 출처 정책'을 따른다.

 

 

 

CORS 적용 방법

기본적으로 다른 출처의 리소스는 응답 헤더에 적합한 CORS 헤더가 존재하면 사용할 수 있다.

하지만 일부 요청은 CORS 명세에 따라 서버에 사전 요청(preflight)을 보내어 서버의 승인을 받은 뒤 실제 요청을 보낼 수 있다.

또한, 필요에 따라 쿠키와 같은 인증데이터를 요청에 추가할 수 있다. 

 

교차 리소스 공유가 동작하는 세가지 방법에 대해서 살펴보도록 한다.

각각의 방법에서 sample code는 A 서버의 웹페이지(http://localhost:8080/cors-page)에서 B 서버(http://localhost:10000)의 api를 호출한 결과를 공유하는 상황을 가정한다.

 

 

1. 단순 요청 (Simple Request)

CORS 사전 요청(preflight)이 필요하지 않은 요청을 말하며 다음과 같은 조건을 갖는다.

 

- HTTP 메서드 : GET, POST, HEAD

- 사용자 에이전트(chrome과 같은 브라우저)에서 자동으로 설정한 헤더 외에 개발자가 직접 설정할 수 있는 헤더는 다음과 같다.

  (CORS-safelisted request-header)

  : Accept, Accept-Language, Content-Language, Content-Type*, Range

  * Content-Type에 허용된 타입 조합: application/x-www-form-urlencoded, multipart/form-data, text/plain

- 요청에 ReadableStream 객체가 사용되지 않는다.

 

위 조건들을 만족하는 교차 요청은 응답 헤더에 'Access-Control-Allow-Origin'이 존재하면 정상처리 된다. (※JSON을 전송하면 단순요청이 아니다.)

 

 

다음은 단순 요청을 처리하는 간단한 코드이다.

 

- '단순 요청' 샘플코드 : REQUEST

try {
    const response = await fetch('http://localhost:10000/cors/simple-request');

    const text = await response.text();
    resultDiv.innerHTML = `<strong>성공:</strong> ${text}`;
    resultDiv.className = 'success';
} catch (error) {
    resultDiv.innerHTML = `<strong>오류:</strong> ${error.message}`;
    resultDiv.className = 'error';
    console.error('CORS 요청 실패:', error);
}

 

- '단순 요청' 샘플코드 : RESPONSE

// API - Handler Method
@GetMapping("/simple-request")
public String simpleRequest() {
    return "simple request";
}

 

 

/simple-request API를 호출할 때 다음과 같은 에러를 방지하기 위해 헤더값을 추가한다.

 

 

- '단순 요청' 샘플 코드 : RESPONSE 헤더 추가 

public class SimpleRequestCorsFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("doFilterInternal");
        response.setHeader("Access-Control-Allow-Origin", "*");

        filterChain.doFilter(request, response);
    }
}

 

 

<처리결과>

 

 

 

 

2. 사전 요청(Preflighted requests)

단순 요청과 달리 '사전 전송(preflighted)' 요청은 실제 요청의 안정성을 확인하기 위해 브라우저가 먼저 OPTIONS 메서드를 사용해 다른 출처의 리소스에 HTTP 요청을 보낸다. 응답받은 HTTP RESPONSE에 교차출처 리소스를 허용하는 헤더가 존재할 때, 기존에 계획된 메서드를 사용해서 실제 요청을 보낸다. 

 

 

사전 요청을 처리하는 코드는 다음과 같다. 

 

- '단순 요청' 샘플코드 : REQUEST

async function fetchPreflightedData() {
    const resultDiv = document.getElementById('result');
    try {
        const response = await fetch('http://localhost:10000/cors/preflighted-request', {
            method: 'GET',
            headers: {
                'Custom-Header-Text': '2005 year'
            }
        });

        const text = await response.text();
        resultDiv.innerHTML = `<strong>성공:</strong> ${text}`;
        resultDiv.className = 'success';
    } catch (error) {
        resultDiv.innerHTML = `<strong>오류:</strong> ${error.message}`;
        resultDiv.className = 'error';
        console.error('사전 검증 요청 실패:', error);
    }
}

 

- '사전 요청' 샘플코드 : RESPONSE

@GetMapping("preflighted-request")
public String preflightedRequest(HttpServletRequest request) {
	return "preflighted request: " + request.getHeader("Custom-Header-Text");
}

 

단순 요청의 조건을 만족하지 않은 경우 사전 요청으로 처리한다. 위의 예시에서는 'Custom-Header-Text'라는 커스텀 헤더를 추가하였기 때문에 사전 요청이 발생한다. Authorization 헤더도 사전요청을 발생시키기 때문에 교차 요청을 할 때 JWT 로 인증처리를 하는 대부분의 RESTful 통신이 사전요청을 필요로 한다.

 

 

- '사전 요청' 샘플 코드 : RESPONSE 헤더 추가 

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    log.info("doFilterInternal - preflighted request");
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Access-Control-Allow-Methods", "GET, POST");
    response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Custom-Header-Text");
    response.setHeader("Access-Control-Max-Age", "3600"); // 사전 요청 응답에 대한 캐시 정보 유지시간 - 초단위

    // request를 추가적으로 처리하지 않고 바로 응답
    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
        response.setStatus(HttpServletResponse.SC_OK);
        return;
    }

    filterChain.doFilter(request, response);
}

 

단순 요청과 비교해서 Access-Control-Allow-Origin 외에 Access-Control-Allow-Methods, Access-Control-Allow-Headers 두 헤더가 필수적으로 필요하며 사전요청 응답처리에 대한 캐시 유지시간을 설정하기 위해 Access-Control-Max-Age를 설정한다. 

 

  • Access-Control-Allow-Methods : CORS를 위해 애플리케이션에서 허용할 메서드
  • Access-Control-Allow-Headers : CORS를 위해 애플리케이션에서 허용할 헤더이름

 

<처리결과>

 

 

 

 

 

3. 자격증명을 포함한 요청

CORS시 쿠키를 전달하고자 할 때 '자격증명' 포함 조건을 사용한다.

fetch api를 사용할 때 credentials 옵션을 'include' 상태로 보내고 사전요청 응답에 'Access-Control-Allow-Credentials: true' 헤더를 추가하면 된다.

 

[참고]

Authorization 헤더는 자격증명을 위해 사용되는 헤더이지만, 개발자가 별도로 REQUEST의 헤더에 추가하며 쿠키와 관련 없기 때문에 Access-Control-Allow-Credentials를 응답헤더에 추가할 필요가 없다.

 

 

- '자격증명을 포함한 요청' 샘플코드 : REQUEST

async function fetchCredentialedData() {
    const resultDiv = document.getElementById('credentialed-result');
    document.cookie = 'sample-session-cookie=request; path=/';

    try {
        const response = await fetch('http://localhost:10000/cors/credentialed-request', {
            method: 'GET',
            credentials: 'include'
        });
        const text = await response.text();
        resultDiv.innerHTML = `<strong>성공:</strong> ${text}`;
        resultDiv.className = 'result success';
    } catch (error) {
        resultDiv.innerHTML = `<strong>오류:</strong> ${error.message}`;
        resultDiv.className = 'error';
        console.error('자격증명 요청 실패:', error);
    }
}

 

- '자격증명을 포함한 요청' 샘플코드 : RESPONSE

@GetMapping("/credentialed-request")
public String credentialedRequest(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();
    StringBuilder sb = new StringBuilder();
    for (Cookie cookie : cookies) {
        sb.append(cookie.getName()).append("=").append(cookie.getValue()).append(", ");
    }

    return "credentialed request: " + sb;
}

 

 

- '자격증명을 포함한 요청' 샘플 코드 : RESPONSE 헤더 추가

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("doFilterInternal - credentialed request");

        String origin = request.getHeader("Origin");
        if (origin != null && (origin.equals("http://localhost:8080") || origin.equals("http://127.0.0.1:8080"))) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        }

        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
        response.setHeader("Access-Control-Max-Age", "3600");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        filterChain.doFilter(request, response);
    }

 

주의해야할 사항으로 자격증명이 포함된 요청에 응답할 경우 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 헤더의 값으로 와일드카드(*)를 사용하면 안되고 명시적인 이름으로 지정해야한다는 사실이다.

 

 

 

Summary

HTTP 응답 헤더를 적절히 설정하여 교차 출처 리소스 공유(CORS)를 처리 할 수 있으며, CORS 요청 유형별 특징을 간단히 요약하면 다음과 같다.

  • 단순 요청 : 기본적인 GET/POST/HEAD 요청으로 별도 사전 확인 없이 바로 실행
  • 사전 요청 : JSON 전송, PUT/DELETE 메서드, (Authorization을 포함한) 커스텀 헤더 사용 시 OPTIONS로 먼저 확인
  • 자격증명을 포함한 요청 : 쿠키를 전달하는 REST 통신

 

 

Sample Code

https://github.com/JaewookMun/programming-exercise/tree/main/http-communication

 

programming-exercise/http-communication at main · JaewookMun/programming-exercise

practice framework or skill such as spring, jpa, and so on - JaewookMun/programming-exercise

github.com

 

 

 

References