말로만 듣던 그것... CORS error를 드디어 만나게 되었다.
웹 개발자라면 누구나 한 번쯤 마주하게 되는 문제라고 하니 이번에 확실히 이해하고 가보자!
이전 글에 정리한 OAuth 2.0의 Authorization Code 방식으로 네이버 소셜 로그인을 구현하던 중 다음과 같은 문제를 만났다.
위의 이미지와 같이 User-Agent(프론트)에서 네이버로 인증코드(Authorization Code)를 요청할 때 CORS error 발생
로그인 완료 후 백엔드 서버에서 발급하는 엑세스 토큰을 쿠키로 보냈는데 브라우저에서 토큰을 확인할 수 없음
이 두 가지 문제를 해결하기 위해 개념부터 차근차근 알아보자🚀
CORS는 Cross-Origin Resource Sharing의 약자이다. 직역하자면 '교차 출처 자원 공유'인데 즉, 출처가 다른 리소스를 공유하는 것에 대한 정책이다. 먼저 Origin이 무엇인지, CORS 정책이 왜 필요한지 알아보겠다.
URL은 위와 같이 구성되어 있는데 여기서 말하는 origin은 protocol, domain, port를 말한다.
same origin은 이 세 가지가 모두 같은 것을 말하기 때문에 domain과 port번호가 같아도 protocol이 다르다면 다른 origin인 것이다.
SOP(Same-Origin policy)는 말 그대로 동일한 origin의 리소스만 공유할 수 있도록 하는 정책이다. 이렇게 되면 다른 서버에 있는 리소스를 가져올 수도 없고 답답할 것 같은데 왜 이런 정책을 사용하는걸까?
이유는 바로 보안이다.
SOP는 악의적인 웹 사이트가 다른 사이트에 대한 정보에 접근하여 중요한 데이터를 탈취하거나 조작하는 상황을 막을 수 있고, CSRF(Cross-Site Request Forgery) 공격을 방지한다.
하지만 다른 origin으로부터의 모든 접근이 차단된 것이 우리가 아는 인터넷 환경은 아닐 것이다. 그래서 예외 사항을 두고 이를 만족할 경우 다른 origin이더라도 리소스를 공유할 수 있도록 하는데, 그 중 하나가 CORS 정책이다.
위의 이미지와 같이 SOP 정책을 따르면 항상 리소스 공유가 가능하고, SOP 정책을 위반해도 CORS 정책에 따르면 다른 출처의 리소스를 공유하는 것이 가능하다. CORS는 다른 출처의 리소스를 공유하기 위한 해결책인 것이다!!
XMLHttpRequest
, Fetch API
호출, css의 @font-face
등을 사용할 때 cross-origin HTTP requests가 가능하다. 그럼 이제 어떻게 Cross-Origin의 리소스를 가져올까?
Cross-Origin으로 요청을 보냈을 때 서버의 허락이 있으면 리소스를 가져올 수 있다!
Client는 HTTP request를 보낼 때 header에 Origin
을 명시하여 보낸다.
ex) Origin: http://localhost:3000
Server는 HTTP response를 보낼 때 header에 Access-Control-Allow-Origin
을 담아 보낸다.
→ Access-Control-Allow-Origin
에는 서버가 해당 리소스에 접근하는 것을 허용하는 origin을 명시한다.
ex) Access-Control-Allow-Origin: http://localhost:3000
브라우저는 응답을 받을 때 Origin
과 Access-Control-Allow-Origin
을 비교한다.
→ 만약 같은 경우 리소스를 가져올 수 있고, 다른 경우 CORS error가 발생하여 응답을 사용하지 않고 버린다.
정리하자면 Origin에 대한 비교는 브라우저가 응답을 받을 때 일어나고, Origin
과 Access-Control-Allow-Origin
이 일치할 때 CORS 정책에 따라 리소스를 사용할 수 있는 것이다!
추가적으로 Access-Control-Allow-Methods
, Access-Control-Allow-Headers
, Access-Control-Max-Age
등을 설정할 수 있다.
CORS는 단순 요청과 실행 전 요청, 두 가지 방법으로 동작한다.
아래의 까다로운 조건을 만족하는 경우 단순 요청이 발생한다.
GET
, HEAD
, POST
method 중에 하나인 경우
가능한 Header : Accept
, Accept-Language
, Content-Language
, Content-Type
, Range
Content-Type
이 application/x-www-form-urlencoded
, multipart/form-data
, text/plain
인 경우
Preflight 요청은 바로 본 요청을 보내는 것이 아니라 예비 요청을 먼저 보내 브라우저 스스로 안전한 요청인지 미리 확인하는 것이다. 이 때는 OPTIONS
method를 사용하고 안전하다는 것이 확인되면 204 status code를 반환한다.
위의 이미지와 같이 client는 Access-Control-Request-Method
와 Access-Control-Request-Headers
로 본 요청의 method와 header 등을 명시하여 Preflight 요청을 보낸다.
Server가 이를 받은 후 어떤 것을 허용하고 있는지에 대한 정보를 header에 담아 204 응답을 보낸다. 이후 브라우저는 응답을 받아 안전한지 확인 후 본 응답을 보낸다.
현재 진행 중인 팀 프로젝트에서도 로그인을 위한 fetch 요청 전에 preflight 요청으로 확인한 후 본 요청을 보내는 것을 볼 수 있다.
Credential은 "자격 증명"으로 HTTP 쿠키 및 HTTP 인증 정보(Authorization header)를 인식할 수 있도록한다. fetch API는 기본적으로 credentials를 보내지 않기 때문에 이를 포함하여 요청을 보낼 때는 꼭 credentials
옵션을 설정해야 한다.
fetch(URL, {
method: "POST",
credentials: "include",
body: JSON.stringify({
userId: 1,
}),
})
credentials
옵션의 종류는 다음과 같다.
include
: 동일 출처와 교차 출처 요청 모두에 사용
same-origin
: 동일 출처에 사용
omit
: 자격 증명을 전송하지 않음
이렇게 인증된 요청을 받기 위해서는 Server에서도 Header를 설정해주어야 하는데 Access-Control-Allow-Credentials
를 true로 설정한다. 그리고 인증 정보는 민감한 정보이기 때문에 출처를 명확히 해야한다. 따라서 Access-Control-Allow-Origin
, Access-Control-Allow-Methods
, Access-Control-Allow-Headers
의 값에 와일드카드(*)를 사용할 수 없게 된다.
기본적으로 Cross-Origin을 허용하는 것도 있다. 바로 HTML의 <a>
, <img>
, <video>
, <script>
, <link>
와 같은 tag들은 href
, src
를 통해 다른 출처의 리소스를 가져와 사용할 수 있다!
문제 1. 위의 이미지와 같이 User-Agent(프론트)에서 네이버로 인증코드(Authorization Code)를 요청할 때 CORS error 발생
이 문제는 User-Agent(프론트)와 네이버 Authorization Server 간의 CORS error이다.
const response = await fetch(NAVER_LOGIN_URL);
네이버 OAuth API에 fetch로 get 요청을 보내고 있었다. fetch에서 CORS 정책을 적용하려면 server의 header를 설정해주어야 하는데 우리가 네이버 서버를 조작할 수는 없습니다???
그래서 선택한 방법은 애초에 Cross-Origin을 지원하는 html tag를 이용하여 get 요청을 보내는 것이다.
<a href={NAVER_LOGIN_URL}>네이버 로그인 버튼</a>
a tag는 href
의 url로 get 요청을 보내는 tag이기 때문에 이렇게 사용해보았고 다행히 redirect url로 인증코드를 잘 받아올 수 있었다!! 이후에 프론트 분께서 리액트에서 사용하는 <Link>
tag로 변경하여 적용해주셨다.
첫 번째 문제 해결~~✨
문제 2. 로그인 완료 후 백엔드 서버에서 발급하는 엑세스 토큰을 쿠키로 보냈는데 브라우저에서 토큰을 확인할 수 없음
이 문제는 프론트 서버와 백엔드 서버 간의 CORS error이다.
const response = await fetch('http://localhost:3000/login_path', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
code,
state,
socialType: 'naver',
}),
});
네이버에서 인증 코드를 받은 후 백엔드 서버로 전달하여 로그인을 완료하는 과정이다. 여기서 fetch를 통해 백엔드 서버로 POST
요청을 보내고, 백엔드 서버에서는 로그인이 완료된 사용자에게 JWT를 쿠키로 보내주게 된다.
그럼 여기서 해야 할 일은 다음 두 가지이다.
// 프론트엔드
const response = await fetch('http://localhost:3000/login_path', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
credentials: "include",
body: JSON.stringify({
code,
state,
socialType: 'naver',
}),
});
// 백엔드
app.enableCors({
origin: ['http://223.130.146.253', 'http://localhost:5173'],
credentials: true,
});
위와 같이 백엔드에 접근을 허용하는 origin을 명시해주고, credentials
를 true
로 설정한다. 그리고 프론트엔드는 fetch
요청 시 credentials: "include"
로 설정한다.
🚫주의 : origin 적을 때 http://223.130.146.253/
이렇게 적었더니 local에서는 잘 되다가 배포환경에서 안돼서 찾는데 애먹었다ㅠㅠ 뒤에 슬래시 꼭 빼자...😇
이렇게 하면 로그인이 잘 되어 쿠키에 인증토큰이 담겨있는 모습을 볼 수 있다.
두 번째도 문제 해결~~✨
말로만 듣던 CORS가 정말 골치 아픈 문제라고만 생각했는데 공부해보니 왜 이렇게 자주 CORS error가 발생하는지 조금은 알 수 있었던 것 같다. 웹 개발을 하다보면 보안과 관련된 부분을 등한 시 하게 되는 것 같은데 이런 기본적인 부분부터 쌓아나가면 좋을 것 같다!
https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch
https://velog.io/@kwontae1313/NestJS-CORS
https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F