본문 바로가기
Study (etc)/삽질일기

[Axios] withCredentials과 CORS

by Haren 2023. 12. 29.

 

오랜만에 블로그에 포스팅을 하는 것 같다. 거의 두 달 만인가?

최근에 이런저런 일들로 나를 돌보지 못해 블로그도 거의 손을 놓다시피 했는데...

이제는 다시 나를 돌볼 수 있게 되었으니 블로그에 신경을 써보려 한다.

2달 만의 포스팅은 오늘 배운 Axios의 withCredentials 옵션과 CORS의 관계에 대해서 알아보자.

 

🧑🏻‍💻 Access Token을 보내고 싶었을 뿐이야.

현재 진행하고 있는 프로젝트에서는 이메일과 패스워드를 통해 로그인이 이뤄진다.

Next.js 프론트 클라이언트에서 이메일과 패스워드를 request body에 담아 post 요청을 보내면 Python Django 백 엔드 클라이언트에서는 Access Token과 Refresh Token을 보내주는데, Access Token은 redux-persist를 통해 localStorage에 담고 Refresh Token은 Cookie에 담아 관리한다.

 

여기까지는 문제가 없이 잘 구현을 했는데, Axios Interceptors를 통해 인가가 필요한 API 요청을 가로채 Access Token을 헤더에 담으려고 시도하는데 계속 Axios Error : Network Error가 나를 반겨주었고, 콘솔에는 CORS 정책 에러가 계속 발생했다.

 

요청한 API의 preflight를 확인하니 Access-Control-Allow-Origin 헤더의 값은 '*'로 모든 출처에서의 요청을 허용하는 API였다.

(이러한 방법은 지양해야 한다는 것을 알고 있지만, 개발 단계에서 localhost:3000으로의 접속을 위해 우선은 '*'로 지정하여 모든 출처에서의 요청을 허용해두었다.)

 

해결방법은 생각보다 간단했으나 이런저런 복잡한 것들이 있어서 정리해서 기록해보고자 한다.

 

🥹 CORS란 뭘까?

그럼 CORS란 과연 뭘까? 그동안 많은 프로젝트를 진행하며 직접 API를 작성할 때마다 이 CORS라는 녀석에 의해 많이 가로막혔다.

CORS를 풀어 쓰면 Cross-Origin Resource Sharing, 우리말로는 '교차 출처 리소스 공유 정책'이라고 한다. 여기서 '교차 출처'란 같은 출처가 아닌 서로 다른 출처를 이야기 하는 것으로 이해하면 된다.

 

Origin, 출처란 웹 사이트 URL을 뜯어보았을 때 '호스트 주소'와 '포트'까지를 의미한다.

https://example.com:3000/user/mypage 이라는 가상의 URL이 있다고 가정했을 때, https://exaple.com:3000까지를 Origin이라고 부른다.

 

즉 위에서 말한 Access-Control-Allow-Origin 헤더의 값에 지정한 출처는 해당 API를 호출할 수 있는 출처를 나타낸다.

'*'을 지정했다는 것은 모든 출처에서의 요청을 허용하겠다는 뜻인데 이는 CSRF 공격 등의 보안적 문제를 갖고 있다.

CORS와 반대되는 개념으로 SOP, 동일 출처에 대한 리소스 공유 정책도 존재하나 오늘의 TIL의 맹점은 이것이 아니기 때문에 SOP와 CORS는 나중에 다른 포스팅으로 세세하게 정리를 해둬야지.

 

😀 원인은 이렇고, 해결은 저렇다.

간단히 CORS에 대해서도 알아보았지만, 왜 나의 경우 Access-Control-Allow-Origin 헤더의 값이 '*'로 모든 출처에서의 요청이 허용된 API를 localhost:3000 개발 환경에서 호출해도 Axios Error : Network Error를 뿜어냈을까?

export const axiosWithToken = axios.create({
  baseURL: '어쩌구저쩌구아무튼우리URL',
  withCredentials: true,
});

 

axios를 사용하기 위해 create 메소드를 이용해 정의할 때 지정한 withCredentials 속성이 true로 지정되어 있다.

 

Credentials이라는건 쿠키, 인증 헤더 등을 내포하는 자격 인증 정보인데, 브라우저에서는 기본적으로 CORS 요청에 이 Credentials를 첨부하는 것을 차단한다. 따라서 (어쨌든 지금은 localhost - api 호스트라 Cross Origin) CORS 요청에서 Credentials을 담아서 보내려면 withCredentials를 true로 설정해주어 수동으로 Cookie에 담긴 Refresh Token을 보내야 맞다.

 

하지만 Credentials를 주고받기 위해서는 프론트에서만의 처리 뿐 아니라 백에서의 처리도 이루어져야 하는데 이를 담당하는 헤더가 바로

Access-Control-Allow-Credentials 헤더다. 이 값을 true로 지정해주어야 Credentials를 담아 보낸 프론트에서의 API 요청을 백이 받을 수 있는 것이다. 이 과정에서 주의해야할 점이 있는데, CORS 요청과 응답을 행할 때 Access-Control-Allow-Origin 헤더의 값이 모든 출처에서의 요청을 허용하는 와일드카드인 '*'가 지정이 되면 안된다는 것이다.

 

따라서 내가 겪은 문제는 Access-Control-Allow-Origin 헤더의 와일드카드와 Access-Control-Allow-Credentials의 처리가 Django BE 클라이언트에서 이루어지지 않았기 때문으로 추정된다.

 

그래서 일단 원인을 알았으니 내일 출근하자마자 백엔드 개발자분께 Access-Control-Allow-Credentials 헤더의 값이 지정이 되어있는지를 여쭙고 해결해 볼 생각이다. Access-Control-Allow-Origin 헤더도 요청을 보낼 수 있는 출처로 한정지어야겠지.