일이 지났습니다. 시의성에 유의하세요
글의 배경
온라인에서 CORS 관련 자료를 읽다가 한 문장에 혼란을 느꼈습니다: “'주의, withCredentials = true를 설정한 후, 전송되는 cookie는 목적 도메인의 cookie입니다’라는 내용이었는데, 이해가 되지 않았습니다: 현재 도메인이 a.com이고 xhr을 b.com으로 보낸다면, 당연히 원본 도메인 a.com의 cookie를 b.com으로 보내 처리해야 하는 것 아닌가? 어떻게 목적 도메인(여기서는 b.com)의 cookie가 전송된다는 말인가?” 그래서 조사를 시작했습니다(결론부터 말하자면, 제가 본 자료의 설명은 정확했습니다. 실제로 목적 도메인 b.com의 cookie가 전송됩니다).
첫 번째 목표: 단순 요청으로 a.com에서 b.com으로 ajax 보내기
단순 요청이 무엇인지는 직접 구글/StackOverflow에서 확인해 주세요. 여기서 중요한 기본 사실은 cookie는 IP 주소가 아닌 도메인에 따라 결정된다는 점입니다. 저는 로컬에서 cookie를 처리할 수 있는 간단한 express 서비스를 구동했고, 동시에 VPS에도 동일한 서비스를 설정했습니다. 그리고 hosts 파일을 수정하여 다른 도메인을 구현했습니다:
1 | |
먼저 VPS 서버에서 express 서비스를 시작했습니다. 별다른 설정 없이 단순히 header를 반환하도록 했으며, 포트는 9091로 설정했습니다:
1 | |
a.com의 Server 측도 기본적으로 동일하지만, ajax를 보내기 위해 정적 페이지를 추가했고, 포트는 9090으로 설정했습니다:
1 | |
index.html 내용:
1 | |
서버 측에서 Access-Control-Allow-Origin을 설정하지 않았기 때문에 오류가 발생했습니다:

이제 b.com 서버 측에서 a.com의 ajax를 허용하도록 추가했습니다(포트까지 정확히 지정해야 함):
1 | |
다시 요청을 보냈습니다:


콘솔에 오류가 없고 상태 코드가 200이므로 b.com이 a.com:9090의 요청을 허용했다는 것을 확인했습니다.
두 번째 목표: 로컬 서버가 프론트엔드의 cookie를 받기
이제 로컬에서 a.com 백엔드가 프론트엔드의 cookie를 받을 수 있는지 테스트해 보겠습니다:
테스트 방법은 간단합니다. js에 임의의 cookie를 추가하면 됩니다:
1 | |

로컬 콘솔의 Application 탭에서 cookie가 추가된 것을 확인할 수 있습니다. 백엔드 출력도 확인해 보겠습니다:

문제 없습니다. www.a.com:9090에 접속할 때 cookie가 정상적으로 전송되었습니다. 예상대로입니다.
세 번째 목표: a.com의 cookie를 b.com으로 보내기
이제 a.com 페이지에는 cookie가 있습니다. 버튼을 다시 클릭하여 ajax 요청이 cookie를 b.com으로 전송할 수 있는지 확인해 보겠습니다:

cookie를 추가하지 않았을 때와 마찬가지로 a.com의 cookie를 받지 못했습니다. 이는 보안 제한 때문이며, 예상된 결과입니다.
추가 목표: 비단순 요청 테스트
여기서 단순 요청에 대한 테스트를 추가로 진행해 보겠습니다. xhr에 새로운 header를 추가한 후 요청을 보냅니다:
1 | |
이번에는 Access-Control-Allow-Origin이 추가된 후의 작업이므로 브라우저에서 다른 오류가 발생했습니다:

문제를 인지한 것은 Access-Control-Allow-Origin 오류 때문이었지만, 이번에는 프론트엔드에서 사용자 정의 header를 설정했기 때문에 비단순 요청(non-simple request)이 발생했습니다. 비단순 요청의 경우 사전 요청(preflight)을 먼저 보내는데, 이 요청의 유형은 OPTIONS입니다. 자세한 내용은 이 글]을 참고하세요. 사전 요청의 목적은 b.com 서버가 xiaodan이라는 header를 허용하는지 확인하는 것입니다. 백엔드에서 반환한 header인 Access-Control-Allow-Headers에는 xiaodan이라는 값이 포함되어 있지 않아 오류가 발생했습니다.
이제 b.com이 반환하는 내용에 해당 header를 추가해 보겠습니다:
1 | |
다시 요청을 보내보면:

똑같은 오류가 발생합니다. 서버는 200으로 응답했지만, Access-Control-Allow-Headers를 반환하지 않아 브라우저에서 결과를 거부했습니다(서버가 거부한 것이 아니라, 서버는 200을 반환했습니다).
문제를 조사해 보니, 이 비단순 요청에서 문제가 발생하고 있었습니다. b.com 함수를 약간 수정해 보겠습니다:
1 | |
서버 측:

클라이언트 측:

원인을 분석해 보면(확인 필요, 나중에 HTTP 권위 있는 가이드를 참고할 예정), 비단순 요청의 preflight 요청은 실제 요청을 보내지 않고, 먼저 서버가 특정 비단순 header 필드를 지원하는지 테스트하기 위한 사전 요청을 보냅니다. 즉, 비단순 헤더가 포함된 요청은 app.get('/') 내부로 들어가지 않습니다. 동시에 b.com 서버에서도 console.log(req.headers)가 app.get('/') 내부에 작성되어 있기 때문에, 방금의 요청에서 서버는 아무것도 출력하지 않았습니다. 이는 CORS 표준을 인지하지 못하는 레거시 서버를 보호하기 위한 설계입니다.
소목표-4: a.com 도메인의 cookie를 b.com으로 전송하기
OK, 잠시 중단하고 클라이언트 측, 즉 a.com에서 발신하는 xhr 요청의 페이지에서 withCredentials = true를 설정했을 때(여기서는 xhrSend 부분만 표시) a.com의 cookie를 b.com으로 전송할 수 있는지 테스트해 보겠습니다(단순 요청과 비단순 요청의 결과는 동일하며, 차이를 쉽게 확인하기 위해 xhr에서 설정한 header를 제거했습니다):
1 | |

이번에는 오류 메시지가 변경되어 서버가 Access-Control-Allow-Credentials를 true로 설정하지 않았다고 표시됩니다. 이 header는 요청이 cookie를 포함할 수 있도록 허용하는 역할을 합니다. 따라서 설정을 추가하겠습니다:
1 | |

여전히 cookie가 전송되지 않습니다. Chrome의 cookie를 확인해 보겠습니다:

분명히 cookie가 설정되어 있는데, 무슨 문제일까요? 이해가 되지 않아, req.headers가 express에서 포맷팅된 결과라고 생각하고 원시 headers인 rawHeaders를 확인해 보았습니다:

여전히 보이지 않습니다. cookie가 사라진 걸까요?
아무것도 없어, 그녀는 말했지 나중에 찾을 거라고, 시간, 시간이 나에게 답을 줄 거야 ------ 내 스케이트보드 신발
문제를 찾지 못할 때는 아이스크림을 먹어보세요. 아래로 내려가서 파슬리 맛 네슬레 아이스크림을 샀습니다(하겐다즈는 너무 비싸서요), 그리고 올라오는 순간 번뜩 아이디어가 떠올랐습니다. 아마도 우리의 경우 제3자 쿠키에 해당하는 것 같은데, 제가 브라우저 추적을 금지했기 때문일까요? 그래서 아이스크림을 다 먹고 크롬 설정에서 "추적 안 함" 요청을 브라우징 트래픽과 함께 보내기 옵션의 체크를 해제했습니다:

참고로, 이번에는 req.headers를 app.use 안에 넣어두었습니다(사실 별 문제는 없을 겁니다):
1 | |
다시 버튼을 클릭하여 요청을 보내고, 크롬 콘솔과 b.com 서버 출력을 확인했습니다:
비단순 헤더가 있기 때문에 이전과 마찬가지로 두 개의 요청이 표시됩니다:


서버에서도 수신되지 않았으므로, 이 크롬 설정과는 무관하다는 것을 알 수 있습니다. 따라서 변수를 통제하기 위해 추적 안 함 옵션은 이전과 같이 다시 체크했습니다. 서버 측(GET 요청만 표시):

여전히 쿠키 필드가 없습니다. 왜일까요?
문서 시작 부분의 문구가 떠올랐습니다: ‘주의, withCredentials = true를 설정한 후 전송되는 쿠키는 대상 도메인의 쿠키입니다’. 혹시 a.com에서 버튼을 클릭해 b.com으로 요청을 보낼 때, b.com에 설정된 쿠키를 전송하는 걸까요?
그래서 먼저 b.com 서버에도 index.html을 새로 만들고, 거기에 임의의 쿠키를 추가했습니다:
b.com 서버 코드:
1 | |
b.com의 index.html 코드:
1 | |
자, 먼저 b.com에 접속해 보겠습니다:

문제없이 웹 페이지가 정상적으로 반환되고, 쿠키도 정상적으로 설정되었습니다. 이제 a.com에서 버튼을 클릭해 요청을 보냅니다:

a.com에서 발송된 ajax 요청이 b.com의 쿠키를 함께 전송하는 것을 확인할 수 있습니다.
문서 시작 부분의 문구가 입증되었습니다.
목표-5: a.com의 자바스크립트로 b.com의 쿠키 가져오기:
a.com에서 b.com의 쿠키를 전송할 수 있다면, 프론트엔드에서 b.com의 쿠키를 가져올 수 있을까요?
문서를 확인해 보니, ajax에는 getAllResponseHeaders()와 getResponseHeader() 두 가지 인터페이스가 있고, 서버 측에는 Access-Control-Expose-Header가 있습니다. 그래서 테스트를 진행해 보았습니다(나는 분명히 따로 출력하고 싶습니다, 뭐 어쩌라고?).
먼저 간단한 것부터 시작해, xhr의 getAllResponseHeaders() 인터페이스를 호출해 보았습니다:
1 | |

헤더의 쿠키 필드가 나타나지 않았습니다. 예상대로였지만, 혹시 getAllResponseHeaders()가 헤더를 순회할 때 쿠키가 enumerable: false로 설정되어 있어서일까 싶어 getResponseHeader()도 시도해 보았습니다:
1 | |

역시 예상대로였습니다. 서버에서 노출할 헤더 내용을 설정하지 않았기 때문입니다. 그래서 b.com에서 Access-Control-Expose-Header를 설정했습니다:
1 | |
그리고 다시 getResponseHeader('Cookie')와 getAllResponseHeaders()를 실행합니다.

에러는 없지만, b.com의 cookie를 얻을 수 없습니다. b.com 서버가 모두 동의했는데도 말이죠.
자료를 찾아보니, 이 Access-Control-Expose-Header는 커스텀 header로만 설정되어 프론트엔드에서 획득할 수 있다고 합니다. 하지만 이건 별 의미가 없어요. 왜냐하면 이 커스텀 header는 내가 프론트엔드에서 설정한 것이기 때문이죠. 유일한 역할은 백엔드가 커스텀 header를 수정하거나 새로 생성한 후, 프론트엔드가 가져오는 것입니다. 아래에서 백엔드에 header를 설정하여 프론트엔드가 가져올 수 있도록 하겠습니다:
b.com의 server:
1 | |
a.com의 index.html:
1 | |

이렇게요.
그래서 이 작은 목표는 달성할 수 없지만, SO 커뮤니티에서는 몇 가지 해결책을 제시했습니다. 예를 들어, 서드파티 서비스/백엔드를 사용하여 전달하는 방법 등이 있죠. 규칙은 고정되어 있지만 사람은 유연하니까요. jsonp와 비슷한 거죠, 그렇죠?
연상
어떤 사람들은 몇 달 동안 기초부터 훈련받으면 어떤 언어를 마스터할 수 있다고 말합니다. 저는 이건 터무니없는 이야기라고 생각합니다. 컴퓨터 기초 지식이 없고, 이진법/컴파일 원리/컴퓨터 원리/운영체제 원리/네트워크 기초/통신 프로토콜이 무엇인지 모르는 상태에서 코드를 작성할 수 있다는 건, 단지 따라하는 학습 능력이 뛰어나다는 걸 보여줄 뿐입니다. 이렇게 작성하면 이런 효과가 나온다는 걸 알지만, 왜 이렇게 작성하면 이런 효과가 나오는지는 모르는 거죠.
따라서 컴퓨터와 소통할 때는 지식의 폭이 넓을수록 좋고, 깊이가 깊을수록 좋습니다. 이번에 CORS를 이해하면서, 예전에 접했던 AUTH가 떠올랐습니다.
AUTH2.0이라는 게 있는데, 이게 CORS와 무슨 관계가 있을까요? 별 관계는 없지만, 제가 비교해보겠습니다:
CORS는 B 사이트를 방문한 사용자가 A 사이트에서 요청을 보낼 때 B 사이트의 cookie를 함께 보낼 수 있게 합니다. 단계는 다음과 같습니다:
- 사용자가
B사이트를 방문합니다. - 사용자가
A사이트를 방문하고,A사이트에서B사이트로 요청을 보냅니다. B사이트는A사이트에서 온 요청에 포함된B사이트의cookie를 검증하고, 문제가 없으면B사이트의 데이터를 반환합니다.
AUTH는 사용자가 A 웹사이트에서 B 웹사이트의 리소스에 접근할 수 있게 합니다. 단, 사용자 승인이 필요합니다. 단계는 다음과 같습니다:
A웹사이트에서 요청을 보냅니다.- 1단계에서
B사이트로 리디렉션되어 승인을 확인한 후, 다시A웹사이트로 리디렉션됩니다. - 이제
A웹사이트는tooken(토큰)을 얻어B웹사이트의 사용자 리소스에 접근할 수 있습니다.
비슷하지 않나요?
CORS의 첫 번째 단계는 AUTH의 두 번째 단계에 대응됩니다. AUTH의 두 번째 단계는 CORS의 첫 번째 단계에서 B 웹사이트의 서버에 Access-Control-Allow-Credentials를 추가한 것과 같다고 볼 수 있습니다. 이렇게 하면 승인이 완료된 거죠.
CORS가 cookie를 함께 보내는 건 AUTH의 간소화된 버전이라고 볼 수 있습니다.
아래는 RFC 6749에서 발췌한 내용입니다.

후기
왜 제3자 광고 cookie가 개인정보를 유출할 수 있다고 하는 걸까요? 이는 광고가 A 웹사이트에 게재되면, 광고주는 이 광고가 A 웹사이트에 노출되었다는 사실을 알게 되기 때문입니다(광고 게재를 위한 id/key 같은 identity로 식별하고 결제합니다). 따라서 광고주는 이 광고에 cookie를 설정하는데, 이 광고는 광고 B의 도메인에서 제공되므로 설정되는 cookie는 당연히 B의 cookie가 됩니다. A 웹사이트가 이 광고를 로드할 때마다 반드시 일부 js를 실행하게 되는데, 이 js에서 A 사이트가 cookie를 전송할 수 있도록 허용하고, 동시에 B 사이트도 A 사이트에서 B 사이트의 cookie를 포함해 전송되는 것을 허용합니다. 따라서 모든 정보를 알 수 있게 되는 것입니다.
`Google AD 구현:


주의 사항
-
위에서 설명한 수정 사항 중 서버 측 수정이 포함된 경우 모두 서비스 재시작이 필요합니다.
VPS에서 서비스를 재시작하는 것은 불편할 뿐만 아니라 시간이 지나면 연결이 끊어질 수 있으므로, 가장 좋은 방법은VPS에 글에서 설명한a.com(주로index.html수정용)을 배치하고 로컬에는b.com내용(주로index.js수정용)을 배치하는 것입니다. -
원격 연결을 유지하는 가장 간단한 방법은 백그라운드에서 실행하는 것입니다(연결이 유지되지 않으면 연결이 끊어진 후 요청이 들어와
I/O작업을 수행할 때 여전히 연결이 끊어질 수 있습니다).node index.js &를 실행하거나, 이미node index.js가 실행 중일 때ctrl+z를 눌러 백그라운드로 중단시킨 후bg명령어로 가장 최근의 백그라운드 작업을 활성화할 수 있습니다.jobs명령어로 현재 작업 목록을 확인할 수 있습니다. 현재session에서退出한 후 다시 연결하면 작업은 여전히 실행 중이지만jobs로는 해당 작업을 볼 수 없으므로ps -A로 모든 프로세스를 나열한 후kill ID로node프로세스를 종료하고 다시 실행하면 됩니다. -
작은 목표를 달성하는 동안 오류가 발생했을 때 사용자 정의 필드를
X-로 시작하지 않아서 발생한 것으로 의심했지만, 표준을 확인해 보니X-로 시작해야 한다는 규정은 없었습니다. 위키피디아와 Stack Overflow에서는 단지 사용자 정의Header를X-로 시작하는 것을 권장할 뿐입니다. -
withCredentials = true로 설정한 후에는 서버 측의Access-Control-Allow-Origin을wildcard*로 설정할 수 없습니다.
저는 인생의 중요한 선택의 기로에 섰을 때, 누군가 최선의 방법을 알려주어 소중한 시간을 헛되이 보내지 않기를 바라곤 합니다. 그런 마음으로 저는 자주 블로그를 쓰며, 광활한 인터넷의 이 작은 구석에 제게는 단 한 번뿐인 인생 경험을 기록하여 도움이 필요한 분들에게 도움이 되기를 바랍니다.