프로젝트를 진행하던 중, frontend
의 proxy
설정에 따라 backend
CORS
설정이 달라지는 상황을 맞이하였습니다.
본래 CORS
를 말로만 들어봤고 실제 접해본 건 처음이라, 이를 정리하고 설명하는 포스팅을 적고자 합니다.
저희는 프로젝트를 진행하며 시연 영상을 위해 frontend
를 구현하였고, 아래처럼 Proxy
설정을 덧붙였습니다.
// package.json
{
"proxy" : "http://localhost:8080"
}
그런데 프로젝트 진행 중 갑자기 궁금중이 들어 위 proxy
를 해제해 보았고, 이전엔 나타나지 않던 CORS
가 발생하게 되었습니다.
원인 분석을 위해 정확히 Browser
가 CORS
를 어떻게 처리하는지 알 필요성을 느꼈습니다.
그래서 이를 한번 정리해 보았습니다.
CORS
알아보기CORS
는 서로 다른 출처를 가진 자원들로간 엑세스를 허용하거나 제한할 수 있도록 하는 HTTP
헤더 기반 메커니즘입니다.
이 때 출처를 확인하고 자원을 제한
하는 행동은 대게 Chrome
같은 브라우저가 담당합니다.
이를 우리가 개발하는 입장에서 설명하면 아래 그림과 같습니다.
브라우저는 Front
가 개발한 JavaScript
, HTML
등을 토대로 동작합니다. 이 때 Front
의 코드에는 "Back 으로부터 ~~ 한 정보를 가져와라"
같은 코드가 존재할 것입니다.
그래서 브라우저는 Front
가 명시된 그대로 요청을 보내고 응답을 받습니다. 하지만 이 때 CORS
문제가 발생합니다.
브라우저가 "행동할 지침"
을 받은 자원은 Front
, localhost:3000
으로부터 받은 자원입니다. 하지만 그 지침 속에는 Back
, localhost:8080
으로부터 자원을 받는 것으로 명시되어 있습니다.
때문에 브라우저는 "내가 행동한 origin 은 3000 인데, 응답 받은 자원은 8080 이네? 위험한거 아냐??"
라고 생각해 CORS
를 일으킵니다.
즉, CORS
는 브라우저가 자원간 출처를 인증하며 발생시키는 한 규약인 것입니다.
때문에 CORS
를 해결하기 위해선 "제공한 자원이 안전하다"
고 브라우저에게 알려줘야 합니다.
Simple request
, Preflight request
결국 브라우저는 안정성을 도모하기 위해 "제공한 자원이 안전"
한지 확인하며, 안전하지 못할 경우 CORS
를 일으킵니다.
브라우저가 "자원이 안전"
한지 확인하는 방식은 다양한데, 이 중 대표적인 2 방식을 설명하겠습니다.
Simple Request
Simple Request
는 브라우저가 확인할 요청이 단순해, "단 한 쌍의 요청 - 응답으로 안전한지 확인하는 방법"
이라 할 수 있습니다.
위 그림을 보면 Browser
가 요청의 Origin
을 localhost:3000
으로 인식하고, Server
로 부터 받은 응답의 Access-Controller-Allow-Origin
헤더가 localhost:3000
인지 확인하는 것을 볼 수 있습니다.
즉, 브라우저가 요청한 응답이 안전한지 확인하고 있습니다.
또한 위 그림을 보면 Browser
, Server
간 Request - Response
가 한 쌍만 존재하는 것을 볼 수 있습니다.
이 때 Simple Request
는 말 그대로 "단순한"
요청을 Server
로 보낼 시 이루어지는데, 자세한 요청 규칙은 다음과 같습니다.
GET
, HEAD
, POST
중 하나여야 한다.Accept
, Accept-Language
, Content-Language
, Content-Type
, DPR
, Downlink
,Save-Data
, Viewport-Width
, Width
Content-Type
헤더는 오직 다음 3 가지만 허용된다.application/x-www-form-urlencoded
, multipart/form-data
, text/plain
때문에 요청 body
가 포함된 대부분의 요청은 Simple Request
대신, Preflight Request
를 통해 안정성을 검증합니다.
Preflight Request
Preflight Request
는 preflight
뜻 그대로, "예비 요청으로 안전한지 먼저 확인하고, 본 요청을 보내는 방법"
이라 할 수 있습니다.
Preflight Request
는 크게 예비 요청
과 본 요청
으로 나뉘는데, Browser
는 예비 요청
을 통해 제공받을 (본 요청의 Response)
자원이 안전한지 확인합니다.
이 때 주의할 점은 예비 요청
의 메서드는 HTTP OPTION
이며, "빈 요청"
을 보낸다는 점 입니다.
즉, 예비 요청
의 body
는 비어있으며 HTTP OPTION
메서드로 보내집니다.
브라우저가 예비 요청
을 통해 안정성을 검사하면 정말로 필요한 본 요청
을 보낼지 판단합니다.
만약 안전하지 않으면 본 요청
은 이루어지지 않고, 안전하다면 요청이 이루어지게 됩니다.
Front
의 Proxy
앞서 브라우저가 CORS
를 확인하는 방식 2 가지를 보았습니다. 이는 말 그대로 브라우저가 CORS
를 handle
하는 방식이므로, Front
또는 Back
이 어떤 설정을 하더라도 이 방식 자체는 변하지 않습니다.
하지만 Front
에서 CORS
를 더 편하게 설정하는 방식이 있는데, 바로 Proxy
입니다.
Front
에서 Proxy
를 설정할 경우, 브라우저의 실제 요청은 Front
로 전달됩니다. 그리고 Front
는 Back
과 통신해 응답을 받고, 이를 다시 브라우저로 응답하게 됩니다.
즉, 브라우저 입장에서 Front
가 Back
인 것 처럼 (Proxy)
행동하는 것입니다.
이는 기존의 CORS
handling
과 남다른 점을 보여줍니다.
Proxy
유무에 따라 브라우저 요청 응답을 제공하는 주인 (Host)
이 변하기 때문입니다.
Proxy
를 설정한 경우, (브라우저 입장에서)
모든 요청의 응답은 localhost:3000
으로 받습니다. 때문에 Back
에서 어떠한 CORS
설정을 하지 않아도 CORS
가 발생하지 않습니다.
더 자세히 설명하자면
Front
의Proxy
는 브라우저에게"거짓말"
을 하는 것은 아닙니다.
Proxy
를 설정하면Front
는 제공하는 자원(JavaScript, HTML 등)
의 요청URL
Host
를 자기자신(localhost:3000)
으로 설정합니다.그래서 브라우저는 애초에
localhost:3000
으로 요청하는 코드를 받고 이를 실행할 뿐입니다.
앞서 CORS
가 무엇인지, Front
의 Proxy
에 따라 이것이 어떻게 변하는지 확인하였습니다.
이를 통해 생각해보면 프로젝트의 CORS
가 발생한 원인은 말하기 좀 부끄럽지만;; Back
에서 CORS
가 설정되지 않았기 때문이었습니다.
이를 해결하는 방법으로 2 가지를 생각하였습니다.
proxy
그대로 사용하기첫째로, 너무나 당연하지만 프로젝트 초기 설정 그대로 Front
에 Proxy
를 이용하는 방법입니다.
이는 package.json
에 proxy
를 선언하거나 http-proxy-middleware
등의 proxy middleware
를 사용하는 방법이 있습니다.
// package.json
{
"proxy" : "http://localhost:8080"
}
//src/setupProxy.js
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = (app) => {
app.use(
"/api",
createProxyMiddleware({
target: "http://localhost:8080",
changeOrigin: true,
})
);
};
이 방법은 개발을 진행하며 아주 간단히 CORS
를 해결할 수 있다는 장점이 있지만, 성능 overhead
, Production code
에 (조금)
부적합하다는 단점이 있습니다.
자세한건 front 라 몰루...
CORS
설정하기반면 Back
에 CORS
를 그대로 설정하는 방법이 존재하는데, 자세한 구현은 "CORS 확인이 언제 이루어져야 하는가"
에 따라 달라집니다.
첫번째로 간단히 Spring MVC
구조에 CORS
를 설정하는 방법입니다.
// Security 없이 그냥 가능하다면
// **즉, Controller 단 까지 요청이 들어와 CORS 처리가 가능하다면**
@RestController
public class MainController {
// Preflight Request 의 경우 OPTION 으로 예비 요청 보내는거 주의
@CrossOrigin(
methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT,
RequestMethod.DELETE, RequestMethod.OPTIONS},
origins = {"http://localhost:3000"},
allowedHeaders = {"*"}
)
@RequestMapping
public ResponseEntity<?> corsExample() {
return null;
}
}
// Global CORS config
// **요청이 MVC 구조까지 들어올 수 있다면**
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
}
}
@CrossOrigin
, WebMvcConfigurer
은 Spring MVC
에 포함되어 일반적인 MVC
CORS
를 설정할 수 있습니다.
이 때 매우 중요한 사실이 있는데, 위 방법은 CORS
요청이 MVC
구조까지 들어와야 유효하다는 사실입니다.
저희 프로젝트의 경우 Spring Security
를 이용해 Filter
단 에서 여러 로직이 수행되었습니다.
특히 로그인
의 경우 LoginFilter
를 정의해 사용했는데, 이 때 로그인하는 Endpoint
또한 브라우저의 CORS
대상이 되었습니다.
문제는 LoginFilter
자체는 HTTP Method
를 구분하지 않는다는 점입니다.
브라우저는 로그인 Endpoint
에 Preflight
를 보냈고, 빈 Request body
때문에 Filter
내 에러가 발생해 CORS
가 발생하였습니다.
때문에 저희 프로젝트처럼 Filter
에 다양한 로직이 존재할 경우, @CrossOrigin
과 WebMvcConfigurer
대신 CorsFilter
를 정의하는 걸 추천합니다.
// Spring Security 를 사용한다면 Filter 설정하는게 맘 편함
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean // CORS 용 Filter 만들기
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source
= new UrlBasedCorsConfigurationSource();
CorsConfiguration config
= new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000"); // origin
// CORS 요청에 포함될 수 잇는 Request Header 들 설정
config.addAllowedHeader("*");
config.addAllowedMethod("*"); // CORS 허용할 HTTP Method
// cookie, credential 이 CORS 요청에 포함될 수 있는지 설정
config.setAllowCredentials(true);
// CORS 적용될 endpoint pattern
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
// Spring security CorsFilter 에 등록
http.addFilterAt(corsFilter(), CorsFilter.class);
return http.build();
}
}
위처럼 설정할 경우 Spring Security
가 CORS
요청을 필터링 할 수 있게 됩니다.
때문에 Preflight
요청처럼 빈 Request body
요청을 주요 Filter
전에 거스를 수 있었고 더이상 CORS
가 발생하지 않을 수 있었습니다.
말로만 듣던 CORS
를 처음 겪어 보았고, 차근차근 알아가며 지식을 넓힐 수 잇는 좋은 기회였습니다.
특히 CORS
가 결국에 브라우저 담당이라는 사실을 알게 되었고, Front
의 Proxy
에 대해서도 알게 되었습니다.
결국 문제의 원인은 CORS
를 제대로 모르고 있었기 때문 이었지만 한번 제대로 공부하는 좋은 경험이었다 생각합니다.