계발하는 개발자

[Network/웹 보안] SOP(동일 출처 정책)와 CORS(교차 출처 리소스 공유)란? 본문

🖥 Computer Science/Network

[Network/웹 보안] SOP(동일 출처 정책)와 CORS(교차 출처 리소스 공유)란?

dev_genie 2023. 9. 7. 10:00

CORS 에러를 잘 표현해주는 그림이 있어 가져와봤다.

위 그림에서처럼 CORS 에러는 흔히 프론트엔드에서 백엔드 서버로 API 요청을 보낼 때 발생하는 에러 중 하나다.

내 경우에도 이번에 MySQL 서버에 있는 데이터를 요청하다가 CORS 에러를 만난 적이 있다.

 

⬇️ 이전 포스팅 참고

 

[CORS 에러] Access to XMLHttpRequest at '주소A' from origin '주소B' has been blocked by CORS policy 해결

에러 아파치 서버랑 vue.js dev 서버를 따로 띄워놓고 개발하다 '주소B(vue.js 포트)'로부터 '주소A(아파치 포트)'에 데이터를 요청했을 때 아래와 같은 에러가 떴다. 원인 보안상 웹 애플리케이션은

ziszini.tistory.com

 

 그렇다면, 우릴 이렇게 힘들게 하는 CORS 에러! 대체 뭘까? 


CORS

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

추가적인 HTTP 헤더를 사용하여, 다른 출처의 자원(ex: 데이터, 이미지, 폰트 등)에 접근할 수 있는 권한을 부여하는 메커니즘을 말한다.

CORS라는 이름 뜻 그대로 교차 출처, 즉 다른 출처 자원의 공유를 가능하게 만든다.

 

흔히 'CORS 에러' 등으로 많이 불려져서 에러를 뿜는 나쁜놈!!!🤬 이라고 생각했는데,

사실은 우리를 도와주는 착한 녀석(?)이었다.(반.전ㅋㅋ)

그래서 이 CORS 에러를 잘 이해하기 위해선 덧붙여서 SOP(동일 출처 정책, Same Origin Policy) 라는 것을 알아야 할 필요가 있다.


SOP

Same Origin Policy, 동일 출처 정책

SOP는 말 그대로 동일 출처끼리만 자원을 공유하도록 하는 정책이라 보면 된다.

이를 바꿔말하면 다른 출처에서 가져온 자원과 상호작용하는 것을 제한하는 보안 방식이라 할 수 있다.

동일 출처끼리만 자원을 공유해야하는데, 다른 출처에서 해당 자원에 접근하기 위해 요청이 온다면 브라우저 입장에서 이는 '위협'이겠지? 따라서 보안상의 목적으로 잠재적으로 해로울 수 있는 출처(Origin)로부터의 요청을 제한함으로써 공격받을 수 있는 경로를 줄이기 위해 생겨난 개념이다.

 

따라서 우리가 다른 출처의 서버로부터 데이터를 전달받는 과정 상에서 마주하게 되는 CORS 에러는,

바로 이 브라우저 SOP 정책에 의해 요청이 막힌 상황을 말한다.

 

이때 여기서 말하는 출처란 아래와 같다.

출처: hudi.blog

❗️ 출처는 프로토콜, 호스트, 포트 조합으로 되어있으며, 이 중 하나라도 다를 시 동일 출처로 보지 않는다.

' 다른 출처의 요청을 허용해주는 게 CORS 인데,
왜 SOP가 아니고 CORS 이름의 에러가 발생하느냐? '

여기서 의문이 들 수 있는데,

브라우저 입장에서는 "다른 출처의 요청이 들어왔을 때

별도의 CORS 설정을 해주면 요청을 보내줄게" 라는 것으로 이를 이해하면 될 것 같다.


CORS 해결하기

CORS 에러는 프론트에서 서버에 접근하여 리소스를 가져오려 할 때, 

출처가 같지 않다면 브라우저에 의해 CORS 에러가 발생한다는 것을 알게 됐다.
그렇다면, 이 CORS 에러는 어떻게 해결할 수 있을까?

결론부터 말하면 CORS 에러는 서버측에서 해결할 수도 있고, 클라이언트측에서 해결할 수도 있다.
(하지만 굳이 내 쪽에서 처리할 수 있는 일이라면 내 쪽에서 해결하는 게 상대측 업무 부담을 덜어주는 일이겠지?🙄)


프론트에서 처리한다 하더라도 상황에 따라 서버쪽에서 처리하도록 해야하는 경우라면, 서버 쪽에서 처리할 수 있는 방법도 안다면 협업에 있어서 많은 도움이 될거다.

 

프론트엔드에서 CORS 처리

# 프록시 서버 이용하기

앞선 포스팅에서 프록시 서버를 통해 우회하여 요청을 보냄으로써 CORS에러를 해결할 수 있었다.

프록시 서버는 클라이언트와 서버 사이에 중개 역할을 하는 서버를 말한다.

클라이언트가 서버에 요청을 보낼 때, 프록시 서버가 클라이언트 요청을 대신 받아 서버에 전달하고, 그 결과를 클라이언트에게 다시 전달하는 역할을 수행한다.

 

프록시 서버 설정 방법은 다음의 3가지 방법이 있다.

 

1. proxy 속성 사용 (package.json)

클라이언트 애플리케이션의 package.json 파일에 프록시 설정을 추가하는 방법이다.

이 방법은 주로 리액트 프로젝트에서 사용되며, package.json 파일에 "proxy" 프로퍼티를 추가하고 프록시 서버의 엔드포인트 주소를 설정한다. 그런 다음 요청을 보낼 때 프록시 서버의 주소를 생략하고 엔드포인트 주소만 작성하면 된다.

 

(Vue.js 프로젝트에서는 vue.config.js 파일에 "proxy" 프로퍼티를 추가하는 방법도 있는데, 

이건 앞서 한 번 경험해봤으니까 이번엔 package.json 파일에 proxy 속성 추가하는 방법을 중점적으로 살펴보려 한다.)

(1)  "package.json" 파일을 열고 다음과 같이 "proxy" 프로퍼티를 추가한다.

"proxy": "http://localhost:5000"

이렇게 설정하면 개발 서버는 모든 API 요청을 "http://localhost:5000"으로 프록시한다.

(2) API 요청을 할 때 엔드포인트 주소를 작성한다.

fetch('/api/data')
  .then(response => response.json())
  .then(data => {
    // 데이터 처리 로직
  })
  .catch(error => {
    // 에러 처리 로직
  });

위 코드를 통해 "/api/data" 엔드포인트로 API 요청을 보내게 된다.

이렇게 하면 클라이언트 코드에서는 프록시 서버의 주소를 따로 지정하지 않고도, 개발 서버가 자동으로 "http://localhost:5000/api/data"로 요청을 전달한다.

엔드포인트(Endpoint)란?
엔드포인트는 직역하면 '끝 지점'이다.
한 마디로 URL의 끝 부분을 가리키며, 이는 웹 API에서 특정 리소스에 접근하기 위한 URL 경로를 말한다.
흔히 클라이언트가 특정 작업을 수행하거나 원하는 정보를 가져오기 위해 사용하는 경로다.
클라이언트는 이러한 엔드포인트를 사용하여 서버에서 데이터를 요청하거나 조작할 수 있다.

예를들어, 상황에 따라 다음과 같은 API 엔드포인트를 가질 수 있다.
1. 상품 목록 가져오기 엔드포인트:
- 엔드포인트 경로: /api/products
- 설명: 이 엔드포인트를 통해 클라이언트는 전체 상품 목록을 가져올 수 있다.

2. 특정 상품 정보 가져오기 엔드포인트:
- 엔드포인트 경로: /api/products/{상품ID}
- 설명: 이 엔드포인트를 통해 클라이언트는 특정 상품의 세부 정보를 가져올 수 있다. 상품ID는 동적으로 변경된다.

3. 장바구니에 상품 추가 엔드포인트:
- 엔드포인트 경로: /api/cart/add
- 설명: 이 엔드포인트를 통해 클라이언트는 장바구니에 상품을 추가할 수 있다.

 

2. Heroku의 우회 서버 사용

Heroku의 cors-anywhere 서비스를 사용하여 CORS 문제를 해결할 수 있다.

이 서비스를 통해 클라이언트 애플리케이션에서 요청을 보낼 때 프록시 서버를 사용하여 다른 도메인의 리소스에 접근할 수 있다. 요청할 때 URL에 http://cors-anywhere.herokuapp.com/를 추가하고 원래 엔드포인트 주소를 포함하면 된다.

http://cors-anywhere.herokuapp.com/http://{원래 엔드포인트 주소}

 

3. http-proxy-middleware 라이브러리 사용

이 라이브러리를 사용하면 클라이언트 애플리케이션에서 웹 소켓 통신 및 HTTP 요청의 프록시를 설정할 수 있다. 먼저 http-proxy-middleware 라이브러리를 설치하고, src/setupProxy.js 파일을 만들어서 프록시 설정을 추가하면 된다.

// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
  app.use(
    '/api',
    createProxyMiddleware({
      target: 'http://{프록시 서버 주소}',
      changeOrigin: true,
    })
  );
};

 

백엔드에서 CORS 처리

# Access-Control-Allow-Origin 응답 헤더 추가

응답 헤더의 Access-Control-Allow-Origin 항목을 해당하는 Origin으로 설정해주면 CORS를 도입할 수 있다. Express의 경우는 다음과 같이 추가해 줄 수 있다.

app.use((req, res) => {
    res.header("Access-Control-Allow-Origin", "*"); // 모든 도메인 허용
    res.header("Access-Control-Allow-Origin", "http://localhost:3000"); // 특정 도메인 허용
});

다만 "*"를 이용해 모든 도메인을 허용해 놓는 것은 보안상 좋지 않아서 Origin을 특정해 주는 것이 좋다고 한다.

 

# CORS 미들웨어 사용하기

Express의 경우, cors 모듈을 사용하여 CORS를 도입할 수 있다.

import express from "express";
import cors from 'cors';

const app = express();

app.use(cors({ origin: [
  'http://localhost:3000',
  'https://devgenie.netlify.app'
]));

위와 같이 CORS를 설정한다면, localhost:3000과 nomalog.netlify.app라는 Origin에 대해 CORS를 허용할 수 있다.

 


요약

위 내용들을 간단하게 아래 그림과 같이 정리할 수 있을 것 같다.

 

LIST
profile

dev_genie

@dev_genie

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!