HTTP에서는 다음 단계를 통해 session이 형성된다.
Client-Server 사이 TCP connection을 형성한다.
Client가 server에 request를 보낸다.
Server는 request를 처리하고 결과를 Client에서 전달한다.
( MDN - A typical HTTP session )
그리고 HTTP는 기본적으로 stateless protocol이기에 현재 request-response cycle은 이전에 발생한 request-response cycle의 정보를 알지 못한다. 그렇기에 같은 client가 request를 여러번 보내더라도 server는 request를 보낸 주체가 이전과 같은 client인지 알 수 없다.
하지만 우리가 흔히 사용하는 웹 서비스에선 각 유저를 구분할 수 있어야 유저에 맞는 페이지나 정보를 제공할 수 있다. 그렇기에 server에서 client의 정보를 구분할 수 있는 stateful session을 형성하기 위해 사용할 수 있는 몇 가지 방법 중 하나로 cookie를 사용할 수 있다.
Cookie란 client-server 통신 중 http header에 함께 전달되는 작은 데이터 조각을 말한다. Set-Cookie header를 통해 cookie가 server에서 response header에 추가되어 client로 전달되면 browser는 client의 machine에 cookie를 저장한다. 저장되는 위치는 브라우저마다 다르며 Windows 기준 Chrome browser의 cookie는 C:\Users\사용자 이름\AppData\Local\Google\Chrome\Network
폴더에 저장된다.
그렇게 저장된 cookie는 cookie를 전달한 domain별로 구분되어 관리되고 이후 특정 사이트나 페이지를 방문할 때 browser는 이전에 해당 server로 전달받은 cookie가 있다면 자동으로 함께 실어 보내고 server에서는 해당 cookie 정보를 이용해 user를 구분하는데 사용할 수 있다. 이러한 특성을 통해 cookie를 사용해 stateful session을 형성할 수 있는 것이다.
Cookie당 저장할 수 있는 데이터 사이즈는 보통 4KB로 많은 데이터를 저장하진 못하며 domain당 설정할 수 있는 cookie의 최대 숫자로 브라우저별로 정해져 있으므로 지나치게 남용하지 않는 것이 좋다.
Server에서 cookie 설정
Server에서 request에 대한 response를 전달할 때 header를 통해 cookie를 설정할 수 있다. 아래는 NodeJS에서 /items path로 요청을 했을 때 response와 함께 header에 cookie를 설정하는 간단한 예제다.
import http from "http";
import url from "url";
import fs from "fs/promises";
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const method = req.method;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Set-Cookie", [
"name=JohnDoe; HttpOnly; Path=/; Max-Age=30",
"address=JohnAddress; Path=/; Max-Age=30",
]);
if (pathname === "/items") {
if (method === "GET") {
res.writeHead(200, {
"Content-Type": "text/html; charset=utf-8"
});
const requestFile = await fs.readFile("./views/items.html");
res.end(requestFile);
return;
}
...
}
});
위의 예제와 같이 Set-Cookie header를 통해 request에 대한 response를 보낼 때 cookie를 함께 설정하여 응답할 수 있다. 위의 예제는 testUser라는 값을 가지고 있는 user라는 cookie와 tesetItem이라는 값을 가지고 있는 item이라는 cookie를 설정한다. 이렇게 server에서 설정되어 전달된 cookie는 Chrome browser 기준 개발자 도구 → application tab → 왼쪽 사이드 바 중에 현재 도메인에 대한 cookie 정보를 확인할 수 있는 tab을 통해 확인할 수 있다.
Cookie Options
Cookie를 설정할 때는 단순히 cookie의 이름과 값 뿐만 아니라 cookie가 언제 만기되어 사라질 것인지 혹은 client javascript에서는 접근하지 못하게 httponly cookie로 전달할 것인지 등의 설정이 가능하다.
Max-Age : cookie의 유효기간을 설정한다. 유효기간이 지난 cookie는 삭제된다. 초 단위로 설정할 수 있다. 다음 예제에서 cookie의 유효기간은 10초간 설정된다.
... res.setHeader("Set-Cookie", [ "user=testUser; Max-Age=10" ]);
Expires : cookie의 유효기간을 초 단위로 아닌 특정 날짜 및 시간으로 설정한다. 만약 Max-Age, Expires가 둘 다 설정되어 있다면 Max-Age에 설정된 값이 적용된다.
... res.setHeader("Set-Cookie", [ "user=testUser; Expires=Wed, 21 Oct 2025 09:00:00 GMT" ]);
Path : service를 제공하는 domain에서 특정 path로 들어오는 request에 대한 response header에만 cookie를 설정한다. 만약 Path를 다음과 같이 설정하면 /items 또는 /items/1과 같이 path가 정확히 /items이거나 /items으로 시작하는 url request에 대한 resposne에만 cookie header를 설정하고 추후에 /items가 포함된 url의 페이지를 방문할 때만 browser에서 server에서 설정한 cookie를 request header에 설정하여 보낸다.
... res.setHeader("Set-Cookie", [ "address=JohnAddress; Path=/items; Max-Age=30" ]);
특정 path를 고정하지 않고 다음과 같이 설정하면 service를 제공하는 domain의 모든 path로 들어오는 request에 대한 response header에 cookie를 설정한다.
... res.setHeader("Set-Cookie", [ "address=JohnAddress; Path=/; Max-Age=30" ]);
HttpOnly : client side javascript에서 특정 cookie를 조회하지 못하게 한다. 다음과 같이 두 개의 cookie를 설정한다면 item cookie는 client side javascript에서 조회할 수 있지만 user cookie는 client side javascript에서 조회할 수 없다. 즉, client side에서 특정 cookie에 접근할 수 없도록 막음으로서 cookie 변조로 발생할 수 있는 보안 문제를 방지할 수 있다.
... res.setHeader("Set-Cookie", [ "user=testUser; HttpOnly; Path=/; Max-Age=30", ]); res.setHeader("Set-Cookie", [ "item=testItem; Path=/; Max-Age=30", ]);
client side에서 다음과 같이 document object를 통해 client에서 접근 가능한 cookie list를 조회할 수 있다. HttpOnly로 설정된 cookie는 document.cookie로 조회할 수 없다.
document.cookie;
Secure : Request가 HTTS를 통해 이루어졌을 때만 cookie를 전달한다. 즉, Secure 옵션이 적용된 cookie는 HTTPS가 아닌 HTTP로 통신할 때는 전달되지 않는다. 만약 Server에서 cookie를 아래와 같이 설정하고 있다면 HTTP을 통해 통신할 때는 item cookie만 전달된다. ( localhost에서 테스트 할 때는 적용되지 않는다 )
... res.setHeader("Set-Cookie", [ "user=testUser; Secure; Path=/; Max-Age=30", ]); res.setHeader("Set-Cookie", [ "item=testItem; Path=/; Max-Age=30", ]);
Domain : cookie를 전달할 domain을 지정한다. 따로 지정하지 않으면 default로 cookie를 생성한 host로 지정이 된다. 이 때 sub-domain은 포함되지 않는다. 만약 cookie를 생성한 host와 다른 domain을 설정하면 대부분의 browser는 해당 cookie를 block한다.
Host의 sub-domain에도 cookie를 전달하고 싶다면 현재 host domain을 명시적으로 선언 해준다.
아래 예제처럼 domain을 설정해주면 test.example.com과 같이 sub-domain에도 cookie가 전달된다.
... res.setHeader("Set-Cookie", [ "user=testUser; Domain=example.com; Path=/; Max-Age=30", ]);
SameSite : Cookie를 Cross-site reqeust에도 전달할 것인지 설정 할 수 있다. 사용할 수 있는 option은 Strict, Lax, None이 다. Cookie에 SameSite option이 명시적으로 설정되어 있지 않으면 대부분의 브라우저에선 SameSite=Lax로 취급한다.
... res.setHeader("Set-Cookie", [ "user=testUser; SameSite=None; Secure", ]);
SameSite=Strict
: Same origin request에만 cookie를 전달한다.SameSite=Lax
: Strict option과 같이 same origin request에만 cookie를 전달하지만 다른 사이트에서 link를 통해 넘어올 때 발생하는 request에도 cookie를 전달한다. 예를들어 abc.com라는 사이트에서 우리가 제공하는 사이트로 link를 통해 접근할 때 우리 서버로 페이지 요청을 위한 request가 발생할 것이다. Lax option은 이런 경우에도 cookie를 전달을 허용한다. 하지만 다른 사이트에서 Iframe과 같은 subresource를 통해 발생한 request에는 cookie를 전달하지 않는다.SameSite=None
: cross-origin request에도 cookie를 전달한다. 다만 None option을 사용하기 위해선 Secure 옵션 역시 필수로 사용해야 한다.
Session Cookie, Permanant Cookie
Cookie에 Expires나 Max-Age와 같이 cookie의 유지기간에 대한 option이 있는 cookie를 permanant cookie라고 하고 Expires나 Max-Age와 같이 cookie의 유지기간 option이 없는 cookie를 session cookie라고 한다.
Permanant cookie는 browser를 닫아도 cookie에 설정된 Expires 또는 Max-Age 기간 동안 유지되지만 session cookie는 browser를 닫으면 사라진다.
Credentials
Fetchi API나 Axios와 같은 library를 통해 cross-origin request를 보낼 때 browser는 default로 cookie와 같은 credentials 정보를 request header에 포함 시키지 않는다.
그렇기에 cross-origin request-response에서도 cookie를 교환해야 하는 경우 client, server 모두 credentials에 대한 설정을 해주어야 한다.
다음은 NodeJS에서 credentials 관련 header를 설정하는 예제다.
res.setHeader("Access-Control-Allow-Origin", "https://example.com");
res.setHeader("Access-Control-Allow-Credentials", "true");
...
다음은 client side fetch api에서 credential 관련 설정을 하는 예제다.
const test = fetch("http://localhost:4000/items/123", {
credentials: "include",
});
axios는 다음과 같이 withCredentials option을 통해 credential 설정을 할 수 있다.
axios.get('...', { withCredentials:true })
만약 위와 같이 fetch 또는 axios의 options을 통해 client side에서 credentials 설정을 추가하였지만 server side에서 credential 관련 header 설정을 하지 않으면 CORS에러가 발생할 수 있으므로 주의하자.
cross origin cookie를 정상적으로 사용하려면 위의 설정 뿐만이 아니라 cookie를 설정할 때 SameSite의 option의 설정 또한 중요하다.
Client side에서 cookie 설정
Client side에서 설정하거나 수정할 수 있는 cookie는 HttpOnly가 적용되지 않은, client에서 접근 가능한 cookie만 설정하거나 수정 가능하다. 다음 예제는 myBook과 myCategory라는 새로운 cookie를 추가한다.
document.cookie = "myBook=testBook; max-age=60;";
document.cookie = "myCategory=testCategory; path=/;";
그리고 아래와 같이 기존에 존재하는 cookie에 새로운 값을 할당하면 기존 cookie는 새로운 값으로 업데이트 된다. 예를들어 기존에 address라는 cookie가 있다면 다음 코드는 address cookie를 새로운 값으로 수정한다. HttpOnly cookie에는 적용되지 않으므로 만약 HttpOnly cookie를 아래와 같이 업데이트 하려고 한다면 무시된다.
document.cookie = "address=newAddress"
Frist-party, Third-party cookie
Cookie에 설정된 domain이 현재 user가 사용하고 있는 site와 같은 origin을 가지고 있는 cookie는 first-party cookie로 취급되고 그렇지 않은 cookie는 third-party cookie ( cross-site cookie )로 취급된다. 그렇기에 cross origin request를 통해 주고 받는 cookie는 third-party cookie policy가 적용된다.