프로젝트/크루트

cors 빠져나가기

발전생 2022. 2. 17. 18:19

첫 화면의 데이터는 서버로부터 잘 받아와서 화면에 뿌려줬다. 그런데 nickname 관련 api 요청은 cors에 의해 막혔다.

왜 이런 일이 벌어질까?

 

스택 리스트와 프로젝트 리스트는 nuxt의 fetch call을 통해 api를 요청한다. nuxt 공식 문서를 통해 살펴보면 fetch()와 async()는 서버 사이드 렌더링 시점에 호출된다. 참고로 axios에 proxy 설정은 안 해준 상태이다. 서버 사이드 렌더링 때 가져온 api 결과는 브라우저에서 cors 보호를 안 해주는 것이다.   

 

보면 nickname 호출은 클라이언트 사이드 렌더링 시점에 호출되는 mounted() 시에 이루어진다. 클라이언트 사이드 렌더링 중에 이루어진 호출은 cors 검사가 이루어진다.

 

이제 저 mountd() 훅 대신 fetch() 훅을 사용하면 nickname 역시 서버 사이드 렌더링 과정에서 데이터를 얻어오기 때문에 cors 에러가 나지 않는다. 보면 console에 에러가 하나도 안 뜬 것을 볼 수 있다. 단지 지금 로그인이 안 돼있어서 console.log(err)에 의해 400 에러 메시지를 보여주는 것이다. 

 

 

api 호출은 전부 다 fetch()를 사용하면 좋겠지만 그럴 수가 없다. get 요청은 fetch를 사용하면 cors 문제가 쉽게 해결되나 post나 put은 클라이언트에서 api를 호출해야만 한다. 

 

그리고 fetch와 비슷한 asyncData가 있는데 제약이 좀 많다. 그래서 사용하고 싶지가 않다.

 

This hook can only be used for page-level components. Unlike fetch, asyncData cannot access the component instance (this). Instead, it receives the context  as its argument. 

 

nuxt 공식문서에서 발췌한 내용이다. asyncData는 pages 아래에 있는 vue 파일에서만 사용할 수 있고 this를 통해 평소 짜던 대로 코드를 작성할 수가 없다. 

 

 

아래와 같은 익숙한 cors 에러 메시지를 볼 수 있다. 메시지에서 말해주듯이 응답 메시지 헤더에 Access-Control-Allow-Origin 가 존재하지 않아서 문제다.

from origin 'http://localhost:3000' 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.

Access-Control-Allow-Origin 은 서버에서 http 응답을 하면서 헤더에 "이 데이터를 누가 쓸 수 있는지" 명시해주는 것이다. 브라우저에서는 이 헤더를 보고 지금 접속해 있는 url이 저 헤더에 포함되어 있지 않다면 접근하면 안 되는 데이터라고 생각한다. 그래서 cors 에러가 난 것이다. 

 

이 문제를 해결하는 방법에는 두 가지가 있다. 

  1. 서버에서 응답을 보낼 때 헤더 중 Access-Control-Allow-Origin에 접속할 클라이언트 url을 명시해준다.
  2. 클라이언트 쪽에서 proxy를 사용하여 프론트엔드 서버가 백엔드 서버에 http 요청을 하게 한다.

아무리 검색을 해보고 그걸 대입해봐도 spring security를 사용하고 있는 상황에서 proxy만으로는 cors 문제가 해결이 되지 않았다. 그래서 1번 방식으로 서버에서 response 헤더에 명시를 해주기로 했다.

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowCredentials(true);
    configuration.setAllowedOrigins(List.of("http://localhost:3000"));
    configuration.setAllowedMethods(List.of("HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"));
    configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type"));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

이렇게 만들어놓은 WebSecurityConfig에서 cors에 관한 설정을 bean으로 등록시키면 된다. 

setAllowCredentials로 헤더 ACCESS-CONTROL-ALLOW-CREDENTIALS를 설정해준다. 이건 jwt를 사용하지 않고 http 세션을 사용해서 인증을 할 거라 필요한 부분이다. 

http request의 header에 withCredentials가 true인데 응답의 헤더에 ACCESS-CONTROL-ALLOW-CREDENTIALS가 없으면 브라우저는 받은 응답을 프론트엔드에게 주지 않고 버린다.

그렇기 때문에 프론트엔드에서 withCredentials로 요청에 cookie 값을 실어 보낼 거라면 응답 헤더에서 ACCESS-CONTROL-ALLOW-CREDENTIALS 설정은 필수이다.

axios를 사용하는 경우 이와 같이 withCredentials를 true로 만들어줄 필요가 있다. (아래는 nuxt에서 설정)

axios: {
    // Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308
    baseURL: 'http://localhost:8080/api/v1',
    credentials: true,
  },

 

setAllowedOrigins를 통해서 응답을 받을 수 있는 request를 한정할 수 있다.  ACCESS-CONTROL-ALLOW-ORIGIN을 설정해주는 코드이다.

브라우저는 ACCESS-CONTROL-ALLOW-ORIGIN에 있는 여러 값들과 http request 헤더의 origin 값을 비교한다. 이 origin 값은 요청을 보낸 쪽의 base url이 들어가게 된다. request 헤더의 origin 값이 ACCESS-CONTROL-ALLOW-ORIGIN 리스트 안에 있으면 서버가 허용하는 url에서 보내온 요청이므로 브라우저는 프론트엔드에게 응답을 넘겨준다.