본문 바로가기
CS/Network

CORS

by yongckim 2022. 10. 14.
728x90
반응형

CORS 에러 발생상황

프론트에서 백엔드에게 정보를 받아오기 위해 다음과 같이 요청했다고 가정해봅시다.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

import './App.css';

const App = () => {
	const [data, setData] = useState('');
	useEffect(() => {
		axios.get('http://localhost:8080/api/healthy')
		.then(res => setData(res.data))
	
	}, []);
	return (
		<div>
			{data ? "연결 성공" : "연결 실패"}
		</div>
	);

};

export default App;

프론트 코드는 단순히 백엔드에 요청을 보내고 해당 요청에 대한 반환 값을 이용해 연결 여부를 파악합니다.

@RestController
@RequestMapping("/api")
public class AppController {

    @GetMapping("/healthy")
    public ResponseEntity<Boolean> checkHealthy() {
        return ResponseEntity.ok(true);
    }
}

백엔드는 간단히 /api/healthy 로 요청을 보낼 경우 true라는 boolean 값을 반환해주는 형태입니다.

프론트와 백엔드 서버를 띄운 후 요청을 보내보면 다음과 같이 아무 화면이 안나오는 것을 확인할 수 있습니다.

문제를 확인하기 위해 크롬에서 F12를 눌러 개발자 도구로 이동한 후 요청 상태를 보면 다음과 같이 CORS ERROR가 발생한 것을 볼 수 있습니다.

CORS ERROR?

위의 프론트 엔드와 백엔드 코드를 보면 아무 문제 없어 보입니다.

그렇다면 에러가 발생한 이유는 무엇이고 이 CORS Error라는 것은 무엇일까요?

 

개발자 도구 → 콘솔로 가면 다음과 같은 메시지를 확인할 수 있습니다.

이를 해석해보면 백엔드 서버에서 프론트서버에 대한 액세스가 CORS 정책에 의해 차단 되었다는 것을 알 수 있습니다.

그리고나서 Access-Control-Allow-Origin 헤더가 존재하지 않는다는데 그렇다면 CORS는 무엇이고 Access-Control-Allow-Origin 헤더는 어떤 역할을 하는 걸까요?

CORS(Cross-Origin Resource Sharing) 란?

MDN 웹 문서에서는 CORS를 다음과 같이 설명합니다.

Cross-Origin Resource Sharing(CORS, 교차 출처 리소스 공유)는 추가 HTTP 헤더를 사용하여, 한 출처(Origin)에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다.

 

즉, HTTP 요청은 기본적으로 같은 출처에서 온 요청만 허용하도록하는데 다른 출처에서 온 요청도 CORS를 통해 허용할 수 있도록 해주는 것이라고 보면됩니다.

CORS의 Origin(출처)은 어떤 것을 의미하는 걸까?

CORS는 같은 “출처”에서만 온 요청을 허용하는 상태에서 다른 “출처”에서 온 요청도 허용하는 것입니다.

그렇다면 여기서 출처, Origin은 무엇을 의미하는 걸까요?

 

기본적으로 HTTP 통신을 할 때, URL을 이용하여 통신하게 되는데 URL의 구조는 다음과 같습니다.

// 구조
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

// 예시
<http://localhost:8080/api/products?category=food#list>

출처는 접근할 때 사용하는 URL의 프로토콜(스키마), 도메인(호스트), 포트로 정의됩니다.

두 URL의 프로토콜, 도메인, 포트가 일치하는 경우 같은 출처를 가지게 됩니다.

 

앞서 프론트와 백엔드 간의 통신을 했을 때를 생각해 봅시다.

프론트엔드는 http://localhost:3000 번의 URL을 가지고 있고 백엔드는 http://localhost:8080의 URL을 가지고 있습니다.

 

앞서 알아본 출처를 생각해보면 출처는 URL의 프로토콜, 도메인, 포트가 일치하는 경우 같은 출처라고 했습니다.

프론트엔드와 백엔드의 주소를 살펴보면 포트번호가 다르기 때문에 서로 다른 출처에서 요청했다는 것을 알 수 있게 됩니다.

이 때문에 위의 코드에서 CORS 에러가 발생했던 것입니다.

왜 같은 출처에서 온 요청만 허용하도록 동작할까?

만약, 같은 출처가 아닌 요청이 허용된다면 어떤 문제가 생길지 알아봅시다.

예를 들어 다음과 같은 상황이 있다고 가정해봅시다.

  • 해커는 사용자의 https://example.com 의 메일 목록을 가져오고 싶은 상황
  • 현재 사용자는 쿠키를 통해 https://example.com 의 메일 목록에 액세스할 수 있는 상황

이런 경우 다른 출처에서 불러온 스크립트로 인해 유저의 개인정보가 탈취되는 상황이 발생할 수 있습니다.

 

이런 상황을 막기 위해 SOP(Same Origin Policy)를 통해 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한합니다.

 

서로 다른 출처간의 통신방법

SOP를 통해 해커의 공격에 대해서 더 안전해졌다는 것을 알 수 있지만 처음 예시처럼 프론트엔드 서버와 백엔드 서버가 분리되어 있는 경우 요청이 차단될 수 있습니다.

서로 다른 출처간의 통신을 위해서 프록시를 사용하거나 JSONP, CORS를 사용할 수 있습니다.

프록시 이용

프록시를 이용하는 경우 앞단에 리버스 프록시를 두어 출처를 같게 만들어주면 됩니다.

이런 경우 리버스 프록시에 의해 같은 출처로 변하기 때문에 문제가 생기지 않습니다.

 

JSONP(Javascript Object Notation Padding)

JSONP란 HTML의 <script> 태그를 이용한 스크립트 요청에 대해서는 CORS가 적용되지 않는다는 점을 이용한 우회 방법입니다.

다만 JSONP의 경우 GET 요청만 가능하고, 서버에서 JSONP를 지원하지 않으면 사용이 불가능합니다.

다시 CORS로 돌아가서

앞서 다른 출처의 통신방법에 프록시와 JSONP 방식을 알아보았습니다.

이 두 방법 이외에도 CORS 설정을 통해 서로 다른 출처의 통신을 할 수 있습니다.

 

CORS는 다른 Origin으로 요청 보낼 때 Origin 헤더에 자신의 Origin을 설정하고, 서버로부터 응답을 받으면 응답 헤더에 Access-Control-Allow-Origin 헤더를 확인하여 Request의 Origin과 일치하는지 확인합니다.

 

access-controll-allow-origin 헤더의 경우 *(asterisk) 표시를 통해 모든 출처에 대해 허용할 수 있습니다.

 

CORS의 요청 방식

단순 요청 (Simple Request)

단순 요청은 공식 용어가 아닌 MDN 웹 문서에서 사용하는 용어입니다.

 

단순 요청은 다음의 조건을 만족하는 요청에 대해서 단순 요청이 됩니다.

  • GET, HEAD, POST 중에 하나의 메서드를 사용해야 합니다.
  • 유저 에이전트가 자동으로 설정한 헤더(Connection, User-Agent 등등..)를 제외하면 수동으로 설정할 수 있는 헤더는 오직 Fetch 명세에서 “CORS-safelisted request-header”로 정의한 헤더 뿐입니다.
    • Accept, Accept-Language, Content-Language, Cotent-Type, Range
  • Content-Type 헤더는 다음의 값들만 허용됩니다.
    • application/x-www-from-urlencoded, multipart/form-data, text/plain
  • 요청이 XMLHttpRequest를 이용하여 만들어진 경우 XMLHttpRequest.upload가 반환된 객체에 이벤트 수신기가 등록되어 있지 않습니다.
  • 요청에 ReadableStream 객체가 사용되지 않습니다.

위의 조건을 만족하는 경우 안전한 요청으로 취급되어, 프리플라이트 요청이 필요 없이 단 한번의 요청만을 전달하게 됩니다.

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

프리플라이트 요청은 먼저 OPTIONS 메서드를 사용하여 HTTP 요청을 전달하고 실제 요청을 전송해도 안전한지 여부를 판단합니다.

이때 안전한 요청이라고 판단되면 실제 요청을 서버에게 보내게 됩니다.

 

프리플라이트 요청은 다음과 같은 내용을 포함해서 전송하게 됩니다.

  • OPTIONS 메서드를 사용합니다.
  • Origin 헤더에 자신의 오리진을 설정합니다.
  • Access-Control-Request-Method 헤더에 실제 요청에 사용될 메서드를 설정합니다.
  • Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더들을 설정합니다.

 

프리플라이트 요청에 대한 응답은 다음과 같은 헤더가 포함되어 응답됩니다.

  • Access-Control-Allow-Origin 헤더에 허용되는 Origin 들의 목록 혹은 와일드 카드를 설정합니다.
  • Access-Control-Allow-Methods 헤더에 허용되는 메서드들의 목록 혹은 와일드카드를 설정합니다.
  • Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록 혹은 와일드카드를 설정합니다.
  • Access-Control-Max-Age 헤더에 해당 프리플라이트 요청에 대한 응답이 브라우저에 캐시 될 수 있는 시간을 설정합니다.

 

다음 이미지는 프리플라이트 요청 예시입니다.

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

위의 요청 방식은 인증 정보가 없는 상황에서의 요청 방식이었습니다.

만약, 인증정보가 추가적으로 전달(쿠키 혹은 Authorization 헤더)되어야 한다면 별도로 추가적인 CORS 정책이 필요합니다.

 

먼저, 쿠키 등의 인증정보를 전달할 때는 클라이언트에서 요청시 별도의 설정을 추가해주어야 합니다. (미 설정시 쿠키 등의 인증 정보는 절대 자동으로 서버에 전송되지 않습니다.)

  • XMLHttpRequest, JQuery의 ajax 또는 axios를 사용하는 경우 withCredential 옵션을 true로 설정해주어야 합니다.
  • fetch API를 사용하는 경우 credentials 옵션을 include로 설정해줘야 합니다.

 

서버는 요청에 대해 다음과 같이 응답해야 합니다.

  • Access-Control-Allow-Origin 헤더가 와일드 카드가 아니어야 합니다.
  • Access-Control-Allow-Credentials 헤더는 true로 설정되어야 합니다.

CORS 설정하기

서로 다른 출처간의 통신을 위해 다양한 방법이 있지만 이번에는 CORS를 통해 서로 다른 출처간의 연결을 진행해보겠습니다.

 

백엔드는 Spring 프레임워크를 사용한다고 가정하겠습니다.

CORS 설정을 위해 다음과 같이 설정 파일을 추가합니다.

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("http://localhost:3000")
            .allowedMethods(
                HttpMethod.GET.name()
            );
    }
}
  • @Configureation → Bean의 설정을 담당하는 클래스
  • WebMvcConfigurer → DispatcherServlet을 간편하게 커스텀할 수 있는 인터페이스로 여기서는 CORS 설정을 위해 사용됩니다.
  • addCorsMappings(CorsRegistry registry) → CORS 설정을 추가할 때 사용하는 메서드입니다.
  • addMapping(”/**”) → 지정한 경로에 대해 CORS를 허용합니다. 현재는 모든 경로에 대해 CORS를 허용하도록 지정하고 있습니다.
  • allowedOrigins(”http://localhost:3000”) → 지정한 출처에 대해 CORS를 허용합니다. 현재는 http://localhost:3000에 대해서 CORS를 허용하고 있습니다.
  • allowedMethods(HttpMethod.GET.name()) → 어떤 HTTP 메서드를 허용할 것인지 설정합니다.

 

CORS 사용시 프리플라이트 요청의 경우 OPTIONS 메서드로 먼저 요청을 보내는데 현재 allowedMethods에 OPTIONS 메서드가 없는데 요청이 가능한 이유는 Spring MVC가 기본적으로 HEAD와 OPTIONS를 지원하고 있기 때문입니다.

 

설정을 추가한 후 애플리케이션을 실행해보면 이제 CORS 에러가 발생하지 않고 정상적으로 화면에 출력되는 것을 볼 수 있습니다.

 

 

정리

  • SOP는 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 보안 정책입니다.
  • CORS에서 출처의 의미는 프로토콜, 도메인, 포트를 의미하며, 이 출처가 같아야 CORS 에러가 발생하지 않습니다.
  • SOP를 사용하는 이유는 악의적인 해커가 만든 서버를 통해 스크립트가 실행되어 개인정보가 유출되는 상황을 막기 위함입니다.
  • 서로 다른 출처간의 통신을 위해 프록시를 이용하거나 JSONP, CORS를 이용할 수 있습니다.
    • 서버 앞단에 리버스 프록시를 두어 요청에 대한 출처를 같게 만들어주면 됩니다.
    • JSONP는 HTML의 <script> 태그를 이용한 스크립트 요청에 대해서는 CORS가 적용되지 않는다는 점을 이용한 우회 방법입니다.
    • Cross-Origin Resource Sharing(CORS, 교차 출처 리소스 공유)는 추가 HTTP 헤더를 사용하여, 한 출처(Origin)에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다.
  • CORS는 다른 Origin으로 요청 보낼 때 Origin 헤더에 자신의 Origin을 설정하고, 서버로부터 응답을 받으면 응답 헤더에 Access-Control-Allow-Origin 헤더를 확인하여 Request의 Origin과 일치하는지 확인하는 방식으로 동작합니다.
    • 단순요청은 특정 조건을 모두 만족한 요청에 대해서 단순요청이 되며, 안전한 요청으로 취급되어 바로 요청을 보낼 수 있습니다.
    • 프리플라이트 요청은 먼저 OPTIONS 메서드를 사용하여 HTTP 요청을 전달하고 실제 요청을 전송해도 안전한지 여부를 판단 후 안전할 경우 실제 요청을 전송합니다.
    • 인증 정보를 포함한 요청을 보내야 하는 경우 추가적으로 클라이언트와 응답에 추가적인 내용이 필요합니다.
  • 스프링에서 WebMvcConfigurer 인터페이스의 addCorsMappings 메서드를 오버라이드해서 CORS를 설정할 수 있습니다.

여기까지 CORS를 알아보면서 단순히 Error에 대한 처리방법만 알았지만 CORS가 왜 필요한지에 대해서는 잘 몰랐었습니다.

 

이번 기회에 정리하면서 SOP에 대해 알게되고 SOP에 예외를 두어야 하는 상황에서 CORS를 통해 서로 다른 출처간의 자원 공유를 허용해줄 수 있다는 것을 알 수 있었습니다.

참고자료

https://goddino.tistory.com/158

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS/Errors

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

https://developer.mozilla.org/ko/docs/Glossary/Origin

https://www.rfc-editor.org/rfc/rfc3986

https://www.youtube.com/watch?v=6QV_JpabO7g

https://dongwooklee96.github.io/post/2021/03/23/sopsame-origin-policy-란-무엇일까.html

https://it-eldorado.tistory.com/163

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS#단순_요청simple_requests

반응형