CS/웹

CORS란?

glorypang 2025. 6. 30. 10:07
728x90
반응형
SMALL

들어가며

웹 개발을 하다 보면 종종 이런 낯선 에러를 만난 적 있을 거예요.

Access to fetch at 'http://localhost:8080/~~~' from origin 'http://localhost:5173' has been blocked by CORS policy
또는

'Access-Control-Allow-Origin' header is missing

처음 보면 "서버가 고장났나?" 싶은데, 사실 이건 브라우저가 보안 때문에 일부러 막는 것입니다.

에러 내용 중:

'Access-Control-Allow-Origin' header is present on the requested resource.

이 문장은 결국, 클라이언트의 주소가 `localhost:5173`인데 요청을 보내는 API 주소는 `localhost:8080`이라서, 둘의 출처(Origin)가 다르다는 뜻입니다.

 

그리고 서버가 `Access-Control-Allow-Origin`이라는 허용 헤더를 주지 않았기 때문에 브라우저가 요청을 차단한 겁니다.

 

결론은:

브라우저: "얘네 출처 다르네? 서버가 허락 안 했으니 내가 요청 막을게!"

즉, 브라우저 기준에서는 전혀 다른 사이트처럼 취급하는 거예요.


Origin(출처) 이란?

출처(Origin)란, 쉽게 말해 “어디서 왔는지”를 구분하는 기준입니다.

출처는 아래 세 가지로 결정됩니다:

  • 프로토콜 → `http` 또는 `https`
  • 호스트(도메인/IP) → `localhost`, `127.0.0.1`, `example.com` 등
  • 포트번호 → `5173`, `8080`, `80`, `443` 등

이 3가지 중 하나라도 다르면, 브라우저는 "다른 출처"라고 생각합니다.

따라서:

클라이언트 : http://localhost:5173
API 서버 : http://localhost:8080

→ 포트번호가 다르기 때문에 브라우저는 다른 출처로 간주, 그래서 CORS 정책이 적용됩니다.


SOP(Same Origin Policy, 동일 출처 정책)

웹 개발을 하다 보면 `CORS 에러`를 자주 듣게 되는데,
사실 그 뿌리를 알려면 먼저 SOP, 즉 "동일 출처 정책"을 알아야 합니다.

 

SOP는 말 그대로 동일한 출처에 대한 정책을 말합니다.

그리고 이 정책의 핵심은:

"같은 출처의 서버에 있는 리소스는 자유롭게 가져올 수 있지만, 다른 출처의 서버 리소스는 제한한다."


즉:

  • 내 서버(`origin`)에 있는 이미지, 데이터 → 자유롭게 접근
  • 다른 서버의 이미지나 영상 → 제한되거나 특정 상황에서만 접근 허용

1990년대부터 지금까지 브라우저는 SOP를 자동으로 적용 중입니다.

SOP가 필요한 이유

왜 굳이 "다른 출처는 차단"하는 걸까요?

만약 다른 출처끼리 자유롭게 소통할 수 있다면, 매우 위험한 상황이 벌어집니다.

예를 들어, 다음과 같은 웹 공격이 가능합니다:

  • CSRF (Cross-Site Request Forgery, 사이트 간 요청 위조)
    → 사용자가 모르게 로그인된 사이트에 원치 않는 요청을 보내는 공격
  • XSS (Cross-Site Scripting, 교차 사이트 스크립팅)
    악성 스크립트를 삽입해 개인정보를 훔치는 공격

이런 걸 막으려고 SOP가 등장한 거예요.

"같은 출처끼리는 자유롭게, 다른 출처는 제한!"

SOP의 한계

실제 웹 개발에서는 다른 출처의 리소스를 사용하는 게 매우 흔합니다.

  • 이미지 CDN (콘텐츠 전송 네트워크)
  • 외부 API 호출
  • 구글 폰트, 유튜브 영상, SNS 연동 등

모든 걸 막으면 웹 서비스가 불가능해지겠죠.


현실적인 필요를 반영해 등장한 것이 바로:

CORS (Cross-Origin Resource Sharing, 교차 출처 자원 공유):
”다른 출처라도 서버가 허락하면 통신을 열어줄게!”

이렇게 SOP의 틀을 유지하면서, 필요한 경우만 예외를 주는 시스템이에요.


다시 말해:

  • SOP (Same-Origin Policy, 동일 출처 정책):
    → 브라우저가 기본적으로 다른 출처 요청 차단
  • CORS (교차 출처 자원 공유):
    → 서버가 허용하겠다는 신호를 주면, 예외적으로 통과

이 구조로, 보안과 개발 편의성 사이에서 균형을 맞추는 겁니다.


우리가 CORS 에러를 보았다는 것은, SOP 정책을 위반하는 사항에 대해 CORS 정책까지 위반하여서 리소스가 제한된 것입니다.

즉, CORS 에러는 서버가 '허용한다'는 신호를 주지 않아서 발생하는 겁니다.

브라우저의 동작 원리

  1. 브라우저가 서버에 요청을 보냄
  2. 서버가 응답을 보냄(정상적으로 데이터 보냄)
  3. 브라우저가 응답을 확인
  4. `Access-Control-Allow-Origin` 같은 허용 헤더 없으면?
    → 브라우저가 결과를 "무시"하고 에러를 띄움

서버는 응답을 정상적으로 처리했는데, 브라우저가 차단하는 겁니다.

그래서 서버 로그를 보면 멀쩡히 200 OK가 찍히는데, 브라우저 콘솔에선 시뻘건 CORS 에러가 뜹니다.

출처 비교는 누가 하는가?

  • 출처 비교 및 차단 → 브라우저가 함
  • 서버는 그냥 요청 받으면 응답할 뿐

서버는:

  • 요청을 받음 → 정상적으로 응답을 보냄 → 출처가 다르든 말든, 응답 자체는 처리

브라우저는:

  • 응답을 수신 → 동일 출처인지 확인 → 다르면, 콘솔에 시뻘건 에러 출력 + 결과 차단

그러니까 서버는:

"난 줄 거 다 줬어. 문제 없어"

브라우저는:

"출처 다르네? 허용 헤더도 없네? 그럼 못 써"

이런 식입니다.


CORS(Cross Origin Resource Sharing, 교차 출처 리소스 공유)

CORS는 쉽게 말해서:

"다른 출처(다른 사이트나 서버)에서 온 요청을 허용할지 말지 서버가 정하는 약속"

아무리 보안이 중요해도, 실무에서 개발을 하다 보면 다른 출처 간의 상호작용이 반드시 필요한 경우가 많습니다. 따라서 CORS 정책을 따르는 경우에 한해서, 다른 출처라도 브라우저가 요청을 허용합니다.

결국, 웹 개발자를 괴롭히는 시뻘건 에러 메시지는:

  • 브라우저의 SOP(동일 출처 정책)에 의해 다른 출처의 리소스를 차단하면서 발생
  • CORS는 그 차단을 예외적으로 허용하기 위한 장치

정리하자면, SOP 정책 위반이라도 CORS 규칙을 지키면 다른 출처의 리소스를 사용할 수 있습니다.

CORS 기본 동작 흐름

1단계. 브라우저가 서버에 요청 보낼 때 'Origin'을 같이 전달

  • 클라이언트에서 서버로 HTTP 요청을 보낼 때,
  • 브라우저는 요청 헤더에 자동으로 Origin 값을 추가합니다.

이 `Origin` 값은 요청을 보낸 페이지의 출처 정보입니다.

Origin: <http://localhost:3000>

"나 지금 `localhost:3000`이라는 주소에서 요청 보내는 중이야!" 라고 서버한테 알림.

2단계. 서버가 허용할 출처를 응답에 명시

서버는 클라이언트 요청을 받고, 응답을 돌려줄 때 아래와 같은 헤더를 추가할 수 있습니다:

Access-Control-Allow-Origin: <http://localhost:3000>

"이 출처에서 오는 요청은 내가 허용할게" 라고 서버가 브라우저에 알려주는 겁니다

3단계. 브라우저가 비교 후 최종 판단

  • 브라우저는 자신이 보냈던 요청의 `Origin` 값과
  • 서버가 응답해준 `Access-Control-Allow-Origin` 값을 비교합니다.

결과에 따라:

  • 둘이 일치하거나 허용된 출처 → 정상적으로 리소스를 사용
  • 불일치하거나 허용되지 않은 출처 → 응답을 무시하고 CORS 에러 발생

CORS 작동 시나리오

앞서 살펴본 CORS의 기본 동작은 이해를 돕기 위한 단순화된 설명이었습니다.

하지만 실제로 CORS는 상황에 따라 3가지 시나리오로 다르게 동작합니다.

 

CORS를 제대로 이해하려면 이 세 가지를 구분해서 알아야 합니다.

특히, 단순 요청을 넘어 쿠키나 토큰 등 인증 데이터를 다른 출처에 전달해야 하는 경우, 이 섹션의 내용은 반드시 알아야 합니다.

 

또한, 우리가 TCP/UDP의 내부 통신을 이해하면 최적화가 가능하듯, CORS의 내부 동작을 알아야 불필요한 요청을 줄이고 성능을 최적화할 수 있습니다.

단순 요청(Simple Request)

단순 요청은 말 그대로:

"조건을 만족하면 예비 요청 없이 바로 본 요청"

을 보내는 방식입니다. 비교적 간단합니다.


조건을 만족할 경우:

  • 브라우저는 바로 본 요청 전송
  • 서버가 `Access-Control-Allow-Origin` 헤더 포함해 응답
  • 브라우저가 `Origin` 비교 후 차단 여부 결정

단순 요청 조건

다음 세 가지 모두 만족해야 단순 요청 적용:

  1. 메서드가 `GET`, `HEAD`, `POST` 중 하나
  2. 요청 헤더가 다음 중 하나만 포함:
    • `Accept`, `Accept-Language`, `Content-Language`, `Content-Type`, `DPR`, `Downlink`, `Save-Data`, `Viewport-Width`, `Width`
  3. `Content-Type`이 다음 중 하나:
    • `application/x-www-form-urlencoded`
    • `multipart/form-data`
    • `text/plain`

단순 요청은 Preflight 단계가 없으므로 더 빠르고 가볍습니다.

다만 조건이 까다로워 대부분의 경우 Preflight 요청을 하게 됩니다.

예비 요청(Preflight Request)

Preflight Request란, 말 그대로 본 요청을 보내기 전에 브라우저가:

"이 요청, 안전한 거 맞아? 서버야 허락해줘"

라고 미리 탐색 요청을 보내는 것입니다.

 

이러한 이유는 Cross-Origin 요청이 유저 데이터에 영향을 줄 수 있으니, 브라우저가 자체적으로 한 번 더 확인하는 겁니다.

예비 요청은 일반적인 GET, POST가 아니라 OPTIONS 메서드를 사용합니다.

 

예비 요청 흐름

  1. 자바스크립트의 `fetch()`로 `POST` 요청을 보내려 함
  2. 브라우저가 서버로 OPTIONS 메서드로 예비 요청을 먼저 전송
    • `Origin` 헤더에 현재 페이지 출처
    • `Access-Control-Request-Method` 헤더에 실제 요청 메서드
    • `Access-Control-Request-Headers` 헤더에 실제 요청에 포함될 헤더들
  3. 서버가 응답에 허용 범위 명시
    • `Access-Control-Allow-Origin` → 허용할 출처
    • `Access-Control-Allow-Methods` → 허용할 메서드
    • `Access-Control-Allow-Headers` → 허용할 헤더
    • `Access-Control-Max-Age` → 예비 요청 결과를 캐싱할 시간(초)
  4. 브라우저가 비교 후, 안전하다고 판단하면 본 요청 전송
  5. 본 요청 응답까지 확인 후, 데이터 자바스크립트로 넘김

이때 CORS 위반이 발생할 수 있는 지점:

  • 4번: 예비 요청 응답이 CORS 조건 불충족 → 에러, 본 요청 차단
  • 5번: 본 요청 응답에 CORS 헤더 누락 → 에러, 응답 데이터 차단

OPTIONS 요청에 포함되는 정보:

Origin: 현재 페이지 출처
Access-Control-Request-Method: 실제 요청할 메서드
Access-Control-Request-Headers: 실제 요청할 헤더

서버 응답 예시:

Access-Control-Allow-Origin: <http://localhost:3000>
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization

예비 요청의 단점

보안 강화 목적은 좋지만, 결국:

  • 요청 과정이 한 번 더 늘어나 성능에 악영향
  • 특히 요청이 많은 서비스에선 체감될 수 있음

다만 서버가 `Access-Control-Max-Age`를 잘 설정하면

동일한 요청에 대해 예비 요청을 브라우저가 캐싱해 재활용할 수 있습니다.

인증된 요청(Credentialed Request)

Credentialed Request는 말 그대로 인증 정보 포함 요청입니다.

여기서 인증 정보란:

  • 세션 쿠키
  • `Authorization` 헤더의 토큰
  • 기타 브라우저의 자격 증명 데이터

다른 출처에 인증 정보를 포함해 요청할 경우, 단순 요청이든 예비 요청이든 CORS가 추가적인 제약을 적용합니다.


클라이언트 측 CORS 설정

기본적으로 브라우저가 제공하는 요청 API(`fetch`, `axios` , `jQuery` 등)는 별도의 설정 없이 쿠키, 세션ID, 인증 토큰 같은 민감한 정보를 함부로 요청에 담지 않습니다.

이것도 일종의 보안 장치입니다.

하지만 때로는:

  • 다른 출처 서버에 요청을 보내면서
  • 쿠키나 인증 정보를 함께 보내야 하는 경우

이때 사용하는 옵션이 바로 `credentials` 입니다.

credentials 값 설명
`same-origin`(기본값) 같은 출처 간 요청에만 인증 정보를 담을 수 있다.
`include` 모든 요청에 인증 정보를 담을 수 있다.
`omit` 모든 요청에 인증 정보를 담지 않는다.
// fetch
fetch("<https://api.example.com/data>", {
    method: "POST",
    credentials: "include", // 다른 출처에도 인증 정보 포함
    body: JSON.stringify(data),
    headers: { "Content-Type": "application/json" }
});
// axios
axios.post('<https://api.example.com/data>', data, {
    withCredentials: true // include와 동일한 효과
});
// jQuery
$.ajax({
    url: "<https://example.com:1234/users/login>",
    type: "POST",
    xhrFields: { withCredentials: true }, // 인증 정보 포함
    success: function (retval) {
        console.log(JSON.stringify(retval));
    }
});

서버 측 CORS 설정

클라이언트가 아무리 `credentials` 옵션을 사용해도, 서버가 적절한 CORS 설정을 하지 않으면 브라우저가 응답을 거부합니다. 따라서 서버에도 정확한 설정이 필수입니다.

인증 요청 시 서버 설정 핵심

  1. 응답 헤더 `Access-Control-Allow-Credentials` 를 반드시 `true` 로 설정
  2. `Access-Control-Allow-Origin` 값에 와일드카드(*) 사용 불가
    • 인증 정보는 민감하기 때문에, 반드시 명확한 출처를 지정
  3. `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers` 도 와일드카드(*) 지양
    • 보안상 허용 가능한 범위를 명확하게 지정하는 것이 좋음

즉:

Access-Control-Allow-Origin: <https://example.com> //출처 정확히 지정
Access-Control-Allow-Credentials: true // 인증 정보 하용

와 같이 명확하게 설정해야 브라우저가 응답을 받아줍니다.

 

CORS를 해결하는 방법

직접 서버에서 HTTP 헤더 설정을 통해 출처를 허용하게 설정하는 가장 정석적인 해결책이다.서버의 종류도 노드 서버, 스프링 서버, 아파치 서버 등 여러가지가 있으니, 이에 대한 각각 해결책을 나열해본다.각 서버의 문법에 맞게 위의 HTTP 헤더를 추가해 주면 된다.

서버별 CORS 설정 예시

Node.js 가본 설정

var http = require('http');
const PORT = process.env.PORT || 3000;

var httpServer = http.createServer(function (req, res) {
    res.setHeader('Access-Control-Allow-Origin', '<https://example.com>'); // 허용할 출처
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Credentials', 'true');

    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('ok');
});

httpServer.listen(PORT, () => {
    console.log('Server running on port', PORT);
});

  • `*` 사용은 위험! 꼭 정확한 출처를 명시하세요.

Express.js 설정

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({
    origin: "<https://example.com>",
    credentials: true,
    optionsSuccessStatus: 200
}));

// 예시 라우트
app.get('/data', (req, res) => {
    res.json({ msg: 'CORS 설정 완료' });
});

JSP/Servlet 설정

public class CORSInterceptor implements Filter {

    private static final String[] allowedOrigins = {
        "<http://localhost:3000>",
        "<http://localhost:5500>"
    };

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String origin = request.getHeader("Origin");
        if (isAllowedOrigin(origin)) {
            response.addHeader("Access-Control-Allow-Origin", origin);
            response.addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE");
            response.addHeader("Access-Control-Allow-Credentials", "true");
        }

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

        chain.doFilter(request, response);
    }

    private boolean isAllowedOrigin(String origin) {
        for (String allowed : allowedOrigins) {
            if (allowed.equals(origin)) return true;
        }
        return false;
    }
}

Spring 설정

//전역 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("<http://localhost:8080>")
            .allowedMethods("GET", "POST")
            .allowCredentials(true)
            .maxAge(3600); // Preflight 캐싱 시간
    }
}
// 특컨트롤러 개별 설정
@CrossOrigin(origins = "<http://localhost:8080>", allowCredentials = "true")
@RestController
public class ExampleController {

    @GetMapping("/test")
    public String test() {
        return "CORS 설정 완료";
    }
}

Tomcat 설정

`web.xml` 설정:

<filter>
    <filter-name>CorsFilter</filter-name>
    <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
    <init-param>
        <param-name>cors.allowed.origins</param-name>
        <param-value><http://localhost:3000></param-value>
    </init-param>
    <init-param>
        <param-name>cors.allowed.methods</param-name>
        <param-value>GET,POST,HEAD,OPTIONS,PUT,DELETE</param-value>
    </init-param>
    <init-param>
        <param-name>cors.allowed.headers</param-name>
        <param-value>Content-Type,Authorization</param-value>
    </init-param>
    <init-param>
        <param-name>cors.exposed.headers</param-name>
        <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>
    </init-param>
    <init-param>
        <param-name>cors.support.credentials</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>CorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

AWS S3 CORS 설정

  1. S3 콘솔 메뉴 → 버킷 선택
  2. 권한(Permissions) 탭
  3. 교차 출처 리소스 공유 창에서 [편집] 선택
  4. 텍스트 상자에 아래 JSON CORS 규칙을 입력
[
  {
    "AllowedHeaders": ["Authorization"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedOrigins": ["<http://www.example.com>"],
    "ExposeHeaders": ["Access-Control-Allow-Origin"]
  }
]

정리(순서도)

상황 : 
- 쿠키·세션ID가 포함된 요청 필요 (로그인 유지 등)
- 내 웹사이트: https://my-site.com
- API 서버: https://api.example.com
  1. 브라우저 → 자바스크립트 실행
    • 개발자가 작성한 JS 코드 실행됨
    • 예시: fetch, axios, $.ajax 같은 요청 발생
  2. 자바스크립트 → 브라우저에 요청 명령
    • fetch("https://api.example.com/data", { credentials: "include" })
    • JS 코드는 "이런 옵션으로 요청해줘"라고 브라우저에 전달
  3. 브라우저 → CORS 정책 검사
    • 요청이 '다른 출처'로 나가는지 확인
    • 민감 정보(쿠키, 세션ID)가 포함되는지 확인
    • 서버 허용 여부 모를 경우 '예비 요청(OPTIONS)' 발생
  4. 브라우저 → 서버로 예비 요청(OPTIONS) 전송 (필요한 경우)
    • "이 요청 보내도 돼? 인증 정보 포함해도 돼?"
    • Preflight Request 전송
  5. 서버 → 브라우저에 CORS 응답
    • Access-Control-Allow-Origin 등 응답 헤더 반환
    • 허용 안 하면 브라우저가 차단
    • 허용하면 다음 단계 진행
    Access-Control-Allow-Origin: <https://my-site.com>  
    Access-Control-Allow-Credentials: true
  6. 브라우저 → 서버에 실제 본 요청 전송
    • 쿠키, 세션ID 포함해서 데이터 요청 전송
  7. 서버 → 브라우저에 응답 반환
    • 데이터 전달
    • CORS 관련 응답 헤더 포함 필수
  8. 브라우저 → 자바스크립트로 응답 전달
    • JS 코드에서 then(), success(), callback 등으로 결과 처리

 

출처

https://teddy0.tistory.com/7

https://inpa.tistory.com/entry/WEB-📚-CORS-💯-정리-해결-방법-👏

728x90
반응형
LIST