[Daily morning study] CORS 개념과 해결 방법

#daily morning study

Image


CORS란

CORS(Cross-Origin Resource Sharing)는 브라우저가 한 출처(origin)에서 실행 중인 웹 애플리케이션이 다른 출처의 자원에 접근할 수 있도록 허용하는 HTTP 헤더 기반 메커니즘이다.

출처(Origin)의 정의

출처는 프로토콜 + 호스트 + 포트 세 가지가 모두 일치해야 같은 출처로 인정된다.

URL비교 대상 (https://example.com)같은 출처?
https://example.com/page경로만 다름O
http://example.com프로토콜 다름X
https://api.example.com호스트 다름X
https://example.com:8080포트 다름X

동일 출처 정책(Same-Origin Policy)

브라우저는 기본적으로 동일 출처 정책(SOP)을 적용한다. 이 정책에 따라 스크립트에서 다른 출처로 보내는 HTTP 요청의 응답은 브라우저가 차단한다.

SOP가 존재하는 이유는 보안이다. 만약 SOP가 없다면, 악성 사이트가 사용자의 인증 쿠키를 이용해 다른 사이트의 API를 자유롭게 호출할 수 있게 된다(CSRF 공격).

CORS는 SOP의 엄격함을 완화하는 공식적인 방법이다.


CORS 동작 방식

단순 요청 (Simple Request)

아래 조건을 모두 만족하면 단순 요청으로 처리된다.

  • 메서드: GET, POST, HEAD 중 하나
  • 헤더: Accept, Content-Type(일부만) 등 허용된 헤더만 사용
  • Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나

단순 요청은 브라우저가 요청을 바로 보내고, 서버 응답의 Access-Control-Allow-Origin 헤더를 확인해 허용 여부를 결정한다.

GET /api/data HTTP/1.1
Origin: https://frontend.com

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com

프리플라이트 요청 (Preflight Request)

단순 요청 조건을 벗어난 경우(예: PUT, DELETE, application/json Content-Type, 커스텀 헤더 등), 브라우저는 실제 요청 전에 OPTIONS 메서드로 사전 요청을 먼저 보낸다.

OPTIONS /api/data HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400

Access-Control-Max-Age는 프리플라이트 응답을 캐시하는 시간(초)이다. 지정된 시간 동안은 같은 요청에 대해 프리플라이트를 다시 보내지 않는다.

인증 정보 포함 요청 (Credentialed Request)

쿠키나 Authorization 헤더 같은 인증 정보를 크로스 오리진 요청에 포함하려면 클라이언트와 서버 양쪽에서 설정이 필요하다.

// 클라이언트: credentials 옵션 필요
fetch('https://api.example.com/data', {
  credentials: 'include'
});
// 서버 응답
Access-Control-Allow-Origin: https://frontend.com  // * 불가, 명시적 출처 필요
Access-Control-Allow-Credentials: true

인증 정보를 허용할 때는 Access-Control-Allow-Origin*를 쓸 수 없고 정확한 출처를 명시해야 한다.


주요 CORS 응답 헤더

헤더설명
Access-Control-Allow-Origin허용하는 출처 (* 또는 특정 URL)
Access-Control-Allow-Methods허용하는 HTTP 메서드
Access-Control-Allow-Headers허용하는 요청 헤더
Access-Control-Expose-Headers브라우저가 읽을 수 있도록 노출할 응답 헤더
Access-Control-Allow-Credentials인증 정보 허용 여부
Access-Control-Max-Age프리플라이트 캐시 시간(초)

CORS 해결 방법

1. 서버에서 CORS 헤더 설정 (정석)

서버가 응답에 Access-Control-Allow-Origin 헤더를 올바르게 추가하는 것이 근본적인 해결책이다.

Node.js (Express)

const cors = require('cors');

app.use(cors({
  origin: 'https://frontend.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}));

Spring Boot

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://frontend.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }
}

2. 프록시 서버 활용

개발 환경에서는 프록시를 사용하면 브라우저가 CORS 정책을 우회할 수 있다. 브라우저는 같은 출처의 개발 서버에 요청하고, 개발 서버가 실제 API 서버로 요청을 전달한다.

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
      },
    },
  },
};

이 방식은 브라우저-개발서버 간 요청은 같은 출처이므로 SOP 제한을 받지 않는다. 서버 간 통신에는 SOP가 적용되지 않기 때문이다.

3. Nginx 리버스 프록시

프로덕션 환경에서 프론트엔드와 API 서버의 출처를 통일하거나 CORS 헤더를 Nginx에서 주입하는 방법이다.

server {
    listen 80;
    server_name frontend.com;

    location /api/ {
        proxy_pass https://api.example.com/;
        add_header Access-Control-Allow-Origin "https://frontend.com";
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
        add_header Access-Control-Allow-Headers "Authorization, Content-Type";

        if ($request_method = OPTIONS) {
            return 204;
        }
    }
}

자주 발생하는 실수

Access-Control-Allow-Origin: * + 쿠키

*는 인증 정보(쿠키, Authorization)와 함께 사용할 수 없다. 인증이 필요한 요청에는 반드시 명시적인 출처를 지정해야 한다.

OPTIONS 메서드 미처리

프리플라이트 요청(OPTIONS)을 서버에서 처리하지 않으면 CORS 오류가 발생한다. API 서버에서 OPTIONS 라우트를 별도로 처리하거나 미들웨어가 자동 처리하도록 설정해야 한다.

개발 환경과 프로덕션 환경의 출처 불일치

개발 시 localhost:3000, 프로덕션에서는 https://app.example.com처럼 출처가 달라지므로 환경 변수로 관리하는 것이 안전하다.

const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') ?? [];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
}));

정리

  • CORS는 브라우저 보안 정책(SOP)을 HTTP 헤더로 완화하는 메커니즘이다.
  • 단순 요청은 바로 전송, 그 외는 OPTIONS 프리플라이트 요청이 먼저 발생한다.
  • 인증 정보를 포함할 때는 * 와일드카드를 쓸 수 없다.
  • 근본적인 해결책은 서버에서 올바른 CORS 헤더를 설정하는 것이고, 개발 편의를 위해 프록시를 활용한다.