CORS A to Y
원티드에서 운영 이슈를 처리하면서 만났던 CORS 이슈에 대해 자세히 알아보기 위해서 CORS 에 대해 정리해 보았습니다.
다음과 같은 순서로 CORS에 대해 알아보도록 하겠습니다.
- CORS 는 무엇인지
- CORS 가 브라우저에서 확인되는 방법
- CORS 요청이 가는 방법의 종류
- 실제 프로덕션에서의 CORS
- Chrome SameSite Policy
- 시작하게 된 이유
1. CORS 란
CORS는 교차 출처 리소스 공유(Cross-Origin Resource Sharing)라는 의미입니다.
즉 현재 실행 중인 origin에서 다른 origin의 리소스에 접근 할 수 있도록 브라우저에게 알려주는 것입니다.
❓ 다른 origin의 리소스는 접근 할 수 없나요?
네 다른 origin 리소스에는 접근할 수 없습니다. 브라우저는 기본적으로 SOP(same-origin policy) 정책을 피고 있습니다. 이렇게 다른 도메인에 대한 접근을 막는 이유는 다른 도메인이 리소스에 계속 접근 할 수 있으면 CSRF와 같은 보안 이슈가 발생 할 수 있기 때문입니다.
❓ 언제 CORS 이슈가 발생하나요?
현재 origin과 다른 origin의 리소스에 접근 할 때 CORS 이슈가 발생하게 됩니다.
❓ 그럼 다른 origin은 어떤 걸까요?
같은 origin은 프로토콜 + 호스트 + 포트가 모두 같은 경우 같은 origin이라고 합니다. 몇가지 케이스에 대해서 알아보도록 하겠습니다.
CASE 1 : 경로만 다른 경우 ⭕️
⇒ 경로만 다른 경우는 프로토콜 포트 호스트가 모두 같기 때문에 같은 origin
CASE 2 : 프로토콜이 다른 경우 ❌
⇒ 프로토콜이 다른 경우는 다른 origin
CASE 3 : 포트가 다른 경우 ❌
CASE 4 : 호스트가 다른 경우 ❌
2. CORS HEADER : 어떻게 CORS 가 발생되나요?
이제 언제 발생하는지는 알았는데, 브라우저에서 어떤 방식으로 발생 되는지도 알아보겠습니다.
HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다 — MDN
- CORS 요청 인지 아닌지는 HTTP Header 로 확인하게 됩니다.
- 요청 Header의
Origin
과 응답 Header의Access-Control-Allow-Origin
이 같은 origin인지 비교해서 CORS 에 성립하는지 아닌지를 알게 됩니다.
Request Header
Origin: <https://foo.example>
- 어떤 origin에서 요청이 왔는지를 나타내는 헤더입니다. 따로 설정하는 헤더가 아니고 브라우저에서 설정됩니다.
Response Header
app.post("/foo", (req,res)=>{
res.header("Access-Control-Allow-Origin","<https://foo.example>");
// res.header("Access-Control-Allow-Origin","*")
res.send("bar");
})Access-Control-Allow-Origin: <https://foo.example>
- response 의
Access-Control-Allow-Origin
헤더에 와일드카드(*)로 모두 접근을 허용해 주거나 접근하고자 하는 origin을 적어 주면 됩니다. Access-Control-Allow-Origin
값에는 한 개의 origin만 들어갈 수 있습니다.
3. CORS 요청이 가는 방식
그런데 CORS 요청을 보낼 때 모두 같은 방식으로 요청이 가는 게 아닙니다. MDN 에서는 크게 세가지 방법으로 케이스를 나누고 있는데요.
헤더의 종류에 따라 요청이 가는 방식 두 가지와, 쿠키 값이 있을 때 요청을 보내는 방식 한 가지가 있습니다.
CASE1 : Simple requests
첫번째는 한번만 요청을 보내는 방식입니다.
특정 조건을 만족할 때에는 비교적 안전한 요청이라고 판단하고 한번에 요청을 처리하게 됩니다. 특정 조건은 헤더의 종류입니다. 기본 헤더와 다음과 같은 헤더들만 존재할 때에는 비교적 안전한 요청으로 보고 바로 요청을 보내게 됩니다.
- METHOD : GET, HEAD, POST
- HEADERS : 허용되는 커스텀 헤더들
- connection
- user-agent
- forbidden header name
Accept
Accept-Language
Content-Language
Content-Type
: (application/x-www-form-urlencoded
,multipart/form-data
ortext/plain
only)DPR
data-save
Viewport-Width
Width
🏷 TIP : HTTP Request Header
클라이언트 장치 및 에이전트별 기본 설정 목록을 확인할 수 있도록 사전 컨텐츠 체크를 위한 HTTP request header 입니다. Client Hints를 사용하면 이미지 DPR 해상도의 자동 조절과 최적화 된 assets을 자동으로 적용할 수 있습니다.위에서 DPR, data-save, viewport-width, width 와 같은 설정을 의미합니다
Simple Request로 가는 요청 예시
- 기본 헤더만 존재하기 때문에 한번만 요청이 가는 것을 확인 할 수 있습니다.
CASE 2 : Preflighted Request
- Pre 에서 알 수 있듯이 두 번 요청을 보내는 경우입니다.
- 위에서 정해진 것 이외의 헤더가 있으면 모드 preflighted request로 요청을 보내게 됩니다.
- OPTIONS 메서드를 통해 다른 도메인의 리소스에 접근할 때 HTTP 요청을 보내 실제 요청이 전송하기에 안전한지 확인하고 그 다음 실제 요청을 보내게 됩니다.
- OPTIONS 메소드를 사용하여 유효한 도메인인지 확인하게 되기 때문에 바로 수정이나 삭제의 작업이 이루어지지 않고 비교적 side-effect에서 안전해지게 됩니다.
- 위에서
content-type
이application/json
으로 달라졌기 때문에 option 으로 요청을 확인하고 post 요청을 보내는 것을 확인 할 수 있습니다.
🧐 그럼 요청이 다 preflight Request로 계속 가게 되나요??
- 🙅🏼♀️ request header에
Access-Control-Max-Age
를 설정하면 일정 시간동안 preflighted 요청을 보내지 않고 바로 값을 요청 할 수 있습니다.
Access-Control-Max-Age
를 설정해서 처음 요청은options
>post
로 preflighted request를 하지만, 이후에는 바로post
요청을 하는 것을 확인 할 수 있습니다.
CASE 3 : Credential
위에 두 케이스에 추가해서 쿠키를 다른 도메인에 보내고 싶을 때 설정해야 되는 경우입니다. 그러기 위해서는 cookie를 주고받기 위해서는 credential
설정을 해주어야 합니다.
기본 브라우저 설정상 크로스 도메인의 경우 쿠키 값을 공유하지 않기 때문입니다.
설정
이럴 때 cross domain 일 때도 쿠키 값을 공유한다고 알려줘야 합니다.
Client
withCredentials property
:true
fetch(url, {
credentials: 'include',
header : {...}
})axios.post(url,{}, {
withCredentials : true,
headers: {},
})
Response Header
Access-Control-Allow-Credentials
:true
preflighted request는 option
> post(put,get..)
으로 두 번의 요청을 보내게 됩니다. CORS 미들웨어를 사용하지 않고 헤더를 직접 설정해 준다면 options 에 대한 헤더 요청에도 헤더를 설정해야 합니다.
이제 request header에 cookie 가 담겨서 전송되네요!
🏷 TIP Chrome의 SameSite policy : Chrome에서는 어떤 쿠키가 포함되어서 전송 될까요?
쿠키의 samesite 설정은 세 가지가 있습니다
none
,lax
,strict
세가지 설정에 따라서 쿠키의 공유 범위가 정해지게 됩니다.
4. Real World CORS
실제 서비스를 구현할 때에는 주로 서버에서 CORS 라이브러리를 사용하게 됩니다. 이 라이브러리는 실제로 동작하는 상황에 맞게 헤더를 설정해 주는 역할을 하고 있습니다.
node 의 CORS 라이브러리를 보게 되면 Header를 설정하는 역할을 담당하고 있는 것을 알 수 있습니다.
server (node CORS library)
- origin 에 대한 설정을 해주고 있습니다. 단 library 를 사용하면 여러
Access-Control-Allow-Credentials
에 접근 할 수 있겠네요!
- credential 설정을 하는 부분입니다.
client (React)
- 서버 쪽에서 설정을 했어도 CORS 일 때 client에서 credential 설정을 해 주면 됩니다.
5. Chrome SameSite Policy
올해 초 chrome80 이 나오면서 SameSite Policy 가 변경되었습니다. 원래는 기본 값 samesite:none
으로 설정되어서 쿠키 값을 공유 할 수 있었지만, 기본 설정이 lax
로 바뀌었기 때문에 공유하고자 하는 사이트 범위에 따라 쿠키에 대한 samesite 권한을 적절하게 부여해 주어야 합니다.
SameSite?
samesite를 구분하는 방식은 same origin 을 확인하는 것과는 조금 다릅니다.
samesite 는 domain suffix와 그 이전의 도메인 부분이 같으면 samesite 라고 하게 됩니다.
www.wanted.co.kr
저 밑줄부분이 site 가 되는 것 입니다.
- www.wanted.co.kr 에서 hr.wanted.co.kr 로 보낼 때는 다른 origin 이지만 같은 사이트로 CORS 설정만 해주게 되면 쿠키를 공유할 수 있습니다.
- www.wanted.co.kr 에서 hr.wantedjp.co.kr 로 보낼 때는 다른 origin 이면서 다른 사이트이기 때문에 공유할 수 있는 쿠키가 제한되게 됩니다!
그렇다면 어떤 쿠키가 공유될까?
쿠키 설정에 samesite 부분을 설정하면 쿠키를 공유 할 수 있습니다.
- strict : 같은 사이트만 쿠키 공유 가능
- lax : 같은 사이트 쿠키 공유, 예외적인 상황에는 samesite가 아니어도 공유. 예외) herf, a 등 link로 보내지는 cross domain 은 쿠키 공유 허용, form 으로 보내지는 post의 경우 쿠키 공유 (default)
- none : 모든 사이트들에 대해 쿠키가 공유 가능, 하지만 chrome80 부터는 secure 설정 필수 (secure : https 통신으로만 쿠키를 주고 받을 수 있음)
6. 이걸 시작하게 된 이슈와 이슈 처리
사실 이러한 CORS 이슈를 다루게 된 이유는 SNS 에서 본 CORS 이슈가 JIRA 티켓으로 올라와 있었기 때문입니다.
이슈를 확인해 본 결과 해당 이슈는 adblock과 같은 브라우저 플러그인에서 요청을 블로킹 하고 있는 경우였습니다. 하지만 데이터를 쌓는 부분이 실패하더라도 로그인이 실패하면 안되는 부분이라고 파악했고 통계 등록이 실패한 경우는 에러 로그를 쌓고 처리되도록 수정했습니다.
CORS 에 대해 정리하면서
처리한 티켓은 CORS와 직접적으로 관련된 부분은 아니었지만 CORS에 대한 부분들을 확인하면서 CORS가 동작하는 방식에 대해 정리해 볼 수 있었습니다. 이번 글을 작성하면서 CORS 이슈가 발생하는 상황을 만들어보고 헤더를 설정하며 대응하면서 실제 통신할 때의 과정과 헤더에 대해서도 더 자세히 알아볼 수 있었습니다. credential 설정을 하면서 samesite에 대한 부분도 알게 되었습니다!
이제 CORS 이슈를 만나더라도. 침착하고! 자신있게! 해결 해 보도록 하겠습니다. 😀
Reference
- https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSDidNotSucceed
- https://blog.hackedu.com/same-origin-policy-and-cross-origin-resource-sharing-cors
- https://web.dev/samesite-cookies-explained/#explicitly-state-cookie-usage-with-the-samesite-attribute
- https://publicsuffix.org/
- https://developer.mozilla.org/ko/docs/Web/HTTP/CORS