💥 문제의 시작
Access to fetch at 'http://서버IP:8080/signup' from origin 'API호출한IP' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled
프론트와의 협업 미션을 하는 중에 위와 같은 에러를 만났다. 분명 테스트 코드도 다 통과했고 포스트맨으로도 확인 했었는데 대체 왜? 일단 CORS가 무엇인지부터 알아보자.
💫 CORS란?
= Cross-Origin Resource Sharing = 교차 출처 리소스 공유
CORS에서 Origin이란?
- 웹 컨텐츠에서의 출처를 의미
- 스킴 + 호스트 + 포트
- 같은 출처를 가졌다 = 스킴, 호스트, 포트가 모두 일치한다
엄밀히 말해서 protocol과 scheme은 다른 개념이다. 이 글에서는 http, https를 기준으로 설명하고 있기 때문에 스킴과 프로토콜을 혼용해서 사용한다.
• 스킴 == 프로토콜 👉 http, https, ftp, ...
• 스킴 != 프로토콜 👉 file, magnet, mailto, ...
CORS
한 웹 어플리케이션이 다른 출처에 존재하는 자원에 접근하고 싶다면? 접근 권한을 줄 수 있도록 브라우저에 알려주는 체제인 CORS를 이용해야 한다. 예를 들어 한 컴퓨터의 로컬에서 Spring Boot를 8080 포트로 띄우고, React를 3000 포트로 띄웠다고 가정하자. 이 둘은 포트가 다르므로 다른 출처임을 알 수 있다. React가 Spring Boot의 API를 호출하려고 할 때, 출처가 다르므로 접근 권한이 없다며 CORS 에러가 발생한다.
제한된 교차 출처 HTTP 요청
그렇다면 그냥 접근 권한을 서로 열어두면 편하지 않을까?라는 생각이 들 수 있다. 브라우저의 교차 출처 HTTP 요청을 제한하는 건 보안을 위해서다. 서로 다른 출처를 가진 두 애플리케이션이 마음대로 서로를 접근할 수 있는건 매우 위험하다. 크롬에서 F12를 눌러 개발자 도구만 열어봐도 어떤 서버와 통신하고 어떤 정보를 주고 받는지 등등 여러 정보를 제재없이 열람할 수 있다. 다른 출처를 가진 애플리케이션에 접근 제한이 없다면 XSS나 CSRF 등을 통해 중요한 데이터를 빼가는게 매우 쉬워진다.
중요한 점은 이 CORS를 확인하는 로직이 서버가 아닌 브라우저에 구현되어있다는 점이다. 그러므로 Postman 같은 툴을 이용해 API 요청을 보낼 때는 CORS 에러가 발생하지 않는다.
💡 CORS 에러 해결 방법
CORS가 어떻게 동작하는지 알아보며 에러를 해결해보자.
기본적으로 웹 클라이언트 어플리케이션은 다른 출처의 리소스를 요청할 때 HTTP 프로토콜을 사용한다. 이때 브라우저는 요청 헤더에 Origin이라는 필드에 출처를 함께 담아보낸다. 이후 서버가 이 요청에 대한 응답을 할 때 Access-Control-Allow-Origin 이라는 값에 이 리소스에 접근하는 것이 허용된 출처를 내려준다. 응답을 받은 브라우저는 브라우저가 보냈던 Origin과 서버에서 받아온 Access-Control-Allow-Origin 두 값을 비교한다. 그 후 이 응답이 유효한지 아닌지를 결정한다. 이제 위에서 봤던 오류를 다시 살펴보자.
Access to fetch at 'http://서버IP:8080/signup' from origin 'API 호출한 IP' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled
뭐가 읽히기 시작한다! 문제는 Access-Control-Allow-Origin 값을 못 받았다는 점이다. 그렇다면 Spring은 이 부분을 어떻게 해결할 수 있을까?
@Configuration
public class WebConfig implements WebMvcConfigurer {
public static final String ALLOWED_METHOD_NAMES = "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH";
@Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods(ALLOWED_METHOD_NAMES.split(","));
}
}
WebMvcConfigurer를 활용하면 된다. 해당 인터페이스의 addCorsMappings라는 메서드를 보면 아래와 같은 설명이 쓰여있다. 요약 해보자면 CORS 관련 설정을 담당한다.
Configure "global" cross origin request processing. The configured CORS mappings apply to annotated controllers, functional endpoints, and static resources. Annotated controllers can further declare more fine-grained config via @CrossOrigin. In such cases "global" CORS configuration declared here is combined with local CORS configuration defined on a controller method.
- addMaping: 해당 설정을 적용할 API 범위 선택 (/** -> 전체 적용)
- allowedOrigins: Origin을 허용할 범위 선택 (생략 시 *와 같은 의미로 전체 허용됨)
- allowedMethods: 허용할 HTTP 메서드 선택
- exposedHeader: 서버에서 반환할 헤더 지정
설정을 마쳤으니 다시 요청을 보내보자. 아래와 같은 오류를 새롭게 만나게 되었다. preflight 요청이 통과되지 않았다는 의미인데 preflight에 대해 먼저 살펴보자.
Failed to load 'http://서버IP:8080/signup': Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'API 호출한 IP' is therefore not allowed access. The response had HTTP status code 405. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
preflight
- 브라우저가 요청을 한번에 보내지 않고 예비 요청 / 본 요청으로 나누어 서버로 전송하는 경우
- '예비 요청'이 preflight에 해당
- HTTP 메서드 중 OPTIONS가 사용됨
- 본 요청을 보내기 전 브라우저 스스로 이 요청을 보내는 것이 안전한가?를 확인
현재 내가 요청한 API에서는 Authorization 헤더에 토큰 값을 담아야한다. 하지만 preflight로 보내지는 요청에는 Authorization 헤더가 없다. 서버에서는 토큰 값이 존재하지 않으니 요청을 거절하고, 클라이언트에서는 preflight가 실패했으니 본 요청도 보내지 않는다. 리뷰어 범블비가 좀 더 구체적인 정보를 주셨다.
For a CORS-preflight request, request’s credentials mode is always "same-origin", i.e., it excludes credentials, but for any subsequent CORS requests it might not be. Support therefore needs to be indicated as part of the HTTP response to the CORS-preflight request as well. ㅡMDN docs
preflight 요청은 항상 'same-origin' 모드라서 각종 credential 들이 빠진다. credential 값을 검증하는 부분에서 아래 같은 로직을 추가해 해결할 수 있다.
1. preflight는 OPTIONS임을 이용해 OPTIONS 모두 허용하기
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
return true;
}
2. Spring에서 제공해주는 Util 활용하기
if (CorsUtils.isPreFlightRequest(request)) {
return true;
}
참고
- https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
- https://zetawiki.com/wiki/URI,_URI_%EC%8A%A4%ED%82%B4
- https://evan-moon.github.io/2020/05/21/about-cors/
- https://prolog.techcourse.co.kr/studylogs/2414
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.html
'Develop > Spring+JPA' 카테고리의 다른 글
[Spring] ResponseEntity vs DTO (3) | 2022.08.11 |
---|---|
모든 요구사항을 한 엔드포인트로 처리하는 방법 (2) | 2022.07.08 |
WHERE절 IN과 NamedParameterJdbcTemplate (0) | 2022.06.08 |
[Spring 5 프로그래밍 입문] chapter 5, 6 - 컴포넌트 스캔과 빈 라이프 사이클 (0) | 2022.06.04 |
[Spring Boot] 내가 설정하지 않아도 동작하는 어노테이션 (2) | 2022.05.28 |