프론트와 협업하여 프로젝트 몇 개를 진행하면서 늘 CORS에러를 겪었다. 열심히 백엔드 개발을 마치고 API를 배포하면 프론트쪽에서 항상 연락이왔다. CORS에러가 난다고....
그 외에도 preflight라는 OPTIONS 메서드를 쓰는 요청을 보내시곤 CORS에러가 난다고 말씀하셨던 적도 있다. 당시 검색해봤을 때는 본 요청을 보내기 위한 사전요청이라고 하였는데, 왜 보내는지에 대한 이해하지 못하고 급하게 OPTIONS 요청만 열어서 에러를 처리했던 기억이 있다.
다른 사람들도 협업 과정에서 CORS에러를 많이 만난다고 하는데, 다신 보고싶지 않은 에러다 ㅜ
이번에 싹 정리해보려고 한다!
https://www.youtube.com/watch?v=-2TgkKYmJt4
해당 영상을 많이 참고하여 공부하고 작성하는 글입니다!
SOP(Same Origin Policy)
SOP는 한 출처(Origin)에서 로드한 문서나 스크립트가 다른 출처의 리소스와 상호 작용할 수 있는 방식을 제한하는 중요한 보안 메커니즘
- mozilla
즉, 다른 출처에서 요청을 하는 것을 제한하여 악의적인 사이트가 다른 사이트의 중요한 데이터를 읽을 수 없게 한다는 거다.
여기서 출처(Origin)란?
- Origin = Protocol + Host + Port
- 모두 같으면 같은 Origin이고, 하나라도 다르면 다른 Origin이다
- +) 인터넷 익스플로러는 port가 달라도 같은 출처로 인식됨
- ex) http://localhost와 같은 출처
- http://localhost:80
- http://localhost/api/cors
- ex) 다른 출처
- http://127.0.0.1 (ip는 localhost가 맞지만, 브라우저 입장에서는 string value를 비교 -> 다른 출처로 판단)
SOP를 사용하면 보안에 도움되는 이유
SOP가 없을 때 보안 취약점 예시를 하나 들겠다.
- 선량한 사용자가 facebook에 로그인, facebook 인증토큰을 받아옴
- 해커가 흥미진진한 내용과 링크를 메일을 통해 보냄
- 링크를 클릭 -> http://hacker.ck주소로 접속
- 주소에는 페이스북에 포스트 등록하는 스크립트가 작성되어있음 -> 링크 타고 들어간것만으로 스크립트 내용 실행됨
- 페이스북 인증토큰이 있는 상태이므로, 포스트가 게시 됨
여기서 SOP를 사용한다면 결과는 달라진다.
SOP를 사용하는 facebook은 origin 을 확인하여 origin이 facebook이 아닌 것을 알아낸다. 같은 출처에서 보낸 요청이 아니기 때문에 sop를 위반, 이 요청을 받아들이지 않는다
하지만 무조건 같은 출처의 요청만 허용하는 건 한계가 있다. 다른 출처의 리소스가 필요한 상황도 존재한다!
- ex) 프론트 서버에서 백엔드 서버에 api요청을 보내는 경우
- 이 때는 CORS를 사용한다
CORS(Cross-Origin Resource Sharing)
CORS(Cross-Origin Resource Sharing)는 브라우저가 리소스 로드를 허용해야 하는 자체 Origin이 아닌 Origin(도메인, scheme or 포트)을 서버가 나타낼 수 있도록 하는 HTTP 헤더 기반 메커니즘
- mozilla
CORS는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제라고 한다.
쉽게 생각하면, SOP에서는 같은 Origin이 아닌 요청을 모두 막아버리는데, 서버딴에서 다른 Origin에서 접근을 허용할 수 있도록 하는 거다. 허용된 origin들은 서버에 요청을 보내고, 해당 요청이 성공적으로 서버에 전달된다.
여기서 포인트는 권한을 브라우저에게 알려준다는 것!
CORS 접근 제어 시나리오
- 단순 요청 (Simple Request)
- preflight 요청
- 인증정보 포함 요청 (Credentialed Request)
Preflight 요청 방식
preflight 요청은 사전확인과 비슷한 것이다.
서버에게 (1)이런 요청을 보내도 되는지 확인을 받은 후 (2)실제 요청을 보낸다. 총 2번의 request가 나간다고 보면 된다.
"(1)이런 요청을 보내도 되는지 확인 받는" 게 preflight 요청이다.
preflight요청은 OPTIONS 메서드를 통해 리소스 요청이 가능한지 확인을 한다. 그리고 200번대의 응답이 오면 실제요청을 보낸다.
Prefilght 요청 형식
- Origin: request를 보내는 출처
- Access-Control-Request-Method: 실제 요청할 메서드 형식
- Access-Control-Request-Headers: 실제 요청에서 추가할 헤더 종류
Preflight 응답 형식
- Access-Control-Allow-Origin: 서버 측 허가 출처
- Access-Control-Allow-Method: 서버 측 허가 메서드
- Access-Control-Allow-Headers: 서버 측 허가 헤더
- Access-Control-Max-Age: Preflight 응답 캐시 기간 (preflight를 매번 보내면 리소스적으로 좋지 않기 때문에 일정 기간동안 캐싱해두는 거라고 보면 된다)
Preflight Response는 응답코드를 200번대로 보내야하고, 응답 바디는 비어있는 것이 좋다고 한다.
Simple Request 요청 방식
preflight 요청 없이 바로 본요청을 날리는 방식이다. 본요청을 날리면 서버는 cross-origin인지 확인 후 cors에러를 내보내는 동작을 한다.
Simple Request요청을 사용하려면 다음과 같은 조건을 만족해야한다.
- GET, POST, HEAD 메서드만 가능
- Content-Type은
- Application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 헤더는 Accept, Accept-Language, Content-Language, Content-Type만 허용됨
preflight가 필요한 이유? simple request로 안되나?
이유는 CORS를 모르는 서버를 위해서라고 한다. 서버가 CORS를 인식하고 핸들할수있는지 먼저 확인을 함으로써 CORS를 인식하지 못하는 서버들을 보호할 수 있다. 아래의 예시에서 cors를 모르는 서버의 상황을 보여주겠다!
(CORS spec이 생기기 이전에 만들어진 서버들은 브라우저의 SOP request만 가능하다는 가정하에 만들어졌는데, cross-site request가 CORS로 인해서 가능해졌기 떄문에 이런 서버들은 cross-site request에 대한 security mechanism이 없음. 이로 인해 보안적으로 문제가 생길수 있는데, 이런 서버들을 보호하기 위해 CORS spec에 preflight request를 포함했다고 함)
1. preflight가 없고, cors를 모르는 서버 예시
CORS설정 없는 서버는 요청을 받으면 해당 요청대로 일단 리소스 변경이 일어난다.
해당 서버는 cors설정이 없으니 당연히 응답의 ALLOW-ORIGIN 헤더가 비어있다. 브라우저는 이를 확인하고 client에게 cors에러임을 알려준다.
하지만 이미 서버의 리소스는 변경된 상태이다..
2. preflight가 있고, cors를 모르는 서버 예시
preflight가 있는 예시이다.
먼저 client는 브라우저에게 preflight를 요청한다.
서버는 cors요청이 없기 때문에 일단 preflight에 대한 응답을 보낸다. 해당 응답에는 ALLOW-ORIGIN이 설정되어 있지 않기 때문에 브라우저는 client에게 cors에러임을 알린다.
이 상황에는 서버는 리소스변경을 전혀 하지 않았기 때문에 안전하게 지켜진다.
preflight는 cors를 모르는 서버에게 정말 필요한 작업임을 알 수 있다!
Credentialed Request 방식
인증 관련 헤더를 포함할 때 사용하는 요청이다. 쿠키나 jwt 토큰을 보내는 헤더는 기본적으로 차단이 되는데, 쿠키나 토큰을 서버로 보내고 싶을 때 사용한다고 보면 된다.
클라이언트 측에서는 credentials: include 설정을 해주고,
서버측에서는 Access-Control-Allow-Credentials를 true로 설정해줘야한다.
* 이 때 주의할 점은 서버에서 Access-Control-Allow-Credentials을 *(와일드 카드)로 주면 안된다는 거다. 에러가 발생한다!
CORS 해결법
- 프론트 프록시 서버 설정 (개발 환경)
- 직접 헤더에 설정
- 스프링 부트 이용 (@CrossOrigin)
1. 프론트 프록시 서버 설정
- 브라우저가 프론트 서버에게 요청을 보낼 땐 같은 origin으로 보내 SOP를 위반하지 않도록 한다
- front server는 /api에 대한 요청일 경우 port를 8080으로 바꿔 back server에 요청을 보낸다. 이 경우 cors가 터져도 브라우저에서 터지지 않게 된다.
2. 직접 헤더 설정
- Allow Header, Method 등등을 다 설정해주는 방법이다
- 하지만 스프링 부트를 사용한다면 굳이 이 방법을 사용하지 않아도 된다고 한다!
3. 스프링부트 제공 cors 지원
웹 개발자는 CORS(교차 원본 리소스 공유)와 관련된 문제를 낯설지 않습니다. 그러나 RESTful 서비스 개발을 위해 Spring Boot를 사용한다면 운이 좋을 것입니다. @CrossOrigin 주석은 이러한 CORS 관련 문제를 처리하는 효과적이고 간결한 방법을 제공합니다.
(대충 스프링부트를 사용하면 CORS처리가 간단하다는 뜻)
첫 번째 방법은 class마다 @CrossOrigin 어노테이션을 붙여주는 방법이다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/member")
@CrossOrigin(origin = "https://localhost:3000", allowCredentials = "true")
public class MemberController {
private final MemberService memberService;
private final S3Service s3Service;
....
}
- 여기서 origin을 * 로 주면 오류가 난다. (credentials true일 경우 오류난다고 했음)
- 이 방법은 class마다 붙여주는 방법이라 번거로울 수 있다. 이 방법 말고도 전역적으로 설정해주는 방법이 있다.
@Configuration
public class MyConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
};
}
}
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
- 이렇게 config를 만들어주면 전역적으로 설정이 가능하다
- credentials를 true로 설정해준다면 origin을 명확하게 등록해주면 된다.
결론
post, get이 아닌 patch나 delete는 preflight를 필수적으로 보낸다!
OPTIONS 메서드의 preflight요청을 처리할 수 있도록 서버에서 잘 처리해줘야한다.
또한 jwt토큰을 사용하거나 쿠키를 사용한다면 credential관련해서도 true로 설정해줘야한다! 잊지말기
도움받은 곳
https://www.youtube.com/watch?v=PNtFSVU-YTI
https://www.youtube.com/watch?v=-2TgkKYmJt4