본문 바로가기

개발

HTTP 세션과 쿠키

HTTP

HTTP는 stateless 하다. HTTP를 기반으로 통신하는 서버는 기본적으로 클라이언트에 대해 어떤 상태 정보도 유지하지 않는다는 뜻이다. 어떤 요청이 들어왔을 때 해당 클라이언트가 이전에 요청을 보냈던 클라이언트인지, 몇 번째로 요청을 보내고 있는 것인지 등을 알 수 없다. 그렇기 때문에 클라이언트의 인증 상태(로그인한 사용자인지 여부 등)를 판단하려면 추가적인 조치가 필요하다. 여기서 등장하는 것이 쿠키와 세션이다.

쿠키

아래는 IETF RFCMDN을 참고 및 요약, 정리한 내용이다. 보다 상세한 설명을 보려면 링크를 참고하면 될 것 같다.

개념

쿠키란 간단히 말해 '작은 데이터 조각'이다. 서버가 클라이언트에게 응답을 보낼 때 '쿠키를 설정하겠다'는 의미의 HTTP 헤더 필드와 값(쿠키)을 넣으면 웹 브라우저가 쿠키를 사용자의 디바이스에 저장한다. 이후 발생되는 요청에는 HTTP 헤더에 쿠키가 포함되게 되고 서버는 그 쿠키를 이용해 클라이언트를 식별할 수 있게 되는 것이다.

예시

쿠키를 포함한 HTTP 헤더가 실제로 어떻게 생겼는지 보는 것이 이해에 도움이 될 듯하다.

 

// 응답 (서버 -> 클라이언트)

HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

 

Set-Cookie가 위에서 설명한 '쿠키를 설정하겠다'는 의미의 필드이고, 쿠키 자체는 위와 같이 <Key>=<Value> 페어로 구성된다. 이후 해당 서버에 보내는 요청에는 쿠키가 다음과 같이 포함된다.

 

// 요청 (클라이언트 -> 서버)

GET /sample_page.html HTTP/2.0
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

 

쿠키에는 여러 Attribute를 설정할 수 있는데, 그 중 몇 가지를 간단하게 알아보자.

Expires, Max-Age

위의 예시처럼 쿠키를 설정하면 해당 쿠키는 브라우저가 종료될 때 삭제된다. 만약 쿠키를 일정 기간 동안 클라이언트의 디바이스에 유지시키고 싶다면 ExpiresMax-Age attribute를 사용한다.

Secure, HttpOnly

Secure을 사용하면 HTTPS 프로토콜 하에서만 쿠키를 전송한다. 다시 말해 HTTP(localhost 제외) 통신에서 Secure를 사용하면 쿠키가 설정되지 않는다.

HttpOnly는 JavaScript로 document.cookie API를 사용해 쿠키에 접근하는 것을 불가능하도록 만든다. 만약 쿠키에 민감한 정보가 들어있을 경우, XSS 공격에 대한 방어로써 사용된다.

 

// Expires, Secure, HttpOnly
Set-Cookie: yummy_cookie=choco; Expires=Thu, 31 Oct 2021 07:28:00 GMT; Secure; HttpOnly

Domain, Path

Domain을 설정하지 않으면 쿠키는 오직 해당 쿠키를 'Set'한 호스트로의 요청에만 포함된다. 반면 Domain을 명시적으로 설정하면 그 subdomain으로의 요청에도 쿠키가 포함된다. 예를 들어 Domain=mozilla.org와 같이 설정하면 developer.mozilla.org에서도 쿠키를 사용할 수 있게 되는 것이다.

Path를 설정하면 쿠키가 특정 URL에서만 포함되도록 설정할 수 있다. 이 때는 subdirectory들도 포함된다. 예를 들어 Path=/docs와 같이 설정하면 '/docs', '/docs/Web/', '/docs/Web/HTTP' 등에서 쿠키를 사용할 수 있다.

SameSite

SameSite attribute를 이해하기 위해서 우선 first-party, third-party의 개념에 대해 이해할 필요가 있다. 현재 클라이언트가 example.com에 있는 상태라고 가정하고, other.com/image.png를 사용한다고 해보자. 여기서 '사용한다'는 것은 other.com에 HTTP 요청을 보낸다는 뜻이 된다. 이 때 클라이언트가 other.com의 쿠키를 가지고 있다면 바로 그 쿠키를 third-party 쿠키라고 한다. 반대로 first-party 쿠키는 현재 페이지, 예시에서는 example.com의 쿠키를 의미한다.

SameSite attribute는 위와 같은 third-party 쿠키를 HTTP 요청에 포함할 것인지를 결정한다. Lax, Strict, None 세 가지 값을 가질 수 있다.

Lax로 설정하면 일반적인 요청(이미지, 프레임)에는 쿠키가 포함되지 않고, 클라이언트가 해당 사이트로 이동할 때만 쿠키가 포함된다. Strict로 설정하면 third-party 쿠키는 항상 전송되지 않는다. None으로 설정하면 모든 요청에 쿠키를 전송한다.

Thrid-party 쿠키와 SameSite 속성은 브라우저마다 기본값이 다르고, 정책이 변경되고 있어서 사용하고자 할 경우 주의가 필요할 것 같다. 관련해서 잘 정리된 내용이 있어서 링크를 남겨둔다.

정리

쿠키의 정의와 사용 방식을 알아보았다. 이처럼 쿠키는 서버가 어떤 데이터를 클라이언트 측에 저장하도록 하고 이후 해당 데이터를 사용자 식별, 장바구니 유지 등의 다양한 용도로 사용할 수 있도록 해준다. 하지만 용량과 갯수의 한계, 정보 유출의 위험성 등 단점도 존재한다.

세션

개념

세션은 서버 측에서 일정 시간 동안 같은 클라이언트로부터 온 요청들을 stateful하게 유지하기 위해 사용된다. 서버는 어떤 요청에 대해 ID를 부여하고 그 ID를 쿠키로 설정한다.

 

Set-Cookie: session_id=abcd1234

 

이후의 요청에는 해당 세션 ID가 포함되고, 서버는 이 ID를 사용해 클라이언트를 식별하고 상태 정보(데이터)를 유지할 수 있게 되는 것이다. 따라서 데이터를 사용하기 위해 서버 측에 추가적인 저장 공간이 필요해진다.

예시

세션은 쿠키와 달리 HTTP 스펙이 아니라 서버 어플리케이션에서 사용되는 개념 또는 방식이기 때문에 구현 방법은 다양할 수 있다. 여기서는 expressexpress-session을 사용한 간단한 예제를 살펴보려고 한다.

 

import express from 'express';
import session from 'express-session';

const app = express();

app.use(
  session({
    // storage: 
    secret: 'keyboard cat',
    saveUninitialized: true,
    resave: false,
    cookie: {
      maxAge: 1000 * 60 * 5,
    },
  })
);

app.get('/', (req, res, next) => {
  res.send('Hello World');
});

app.listen(3000, () => console.log('Server Running'));

 

위와 같이 코드를 작성하고, localhost:3000으로 접속하여 개발자 도구의 Network 탭에서 Response Header를 살펴보면 다음과 같은 필드가 포함되어 있는 것을 확인할 수 있다.

 

Set-Cookie: connect.sid=<session_id 값>; Path=/; Expires=Wed, 22 Sep 2021 13:25:41 GMT; HttpOnly

 

또 Application 탭에서 Cookies 메뉴를 보면 connect.sid를 Name으로 갖는 쿠키가 설정되었음을 볼 수 있다.

connect.sid는 express-session이 사용하는 쿠키의 키값이며 위 예시에서 session() 옵션에서 key를 설정하여 변경할 수 있다.

storage는 위에서 설명한 '세션을 사용할 때 서버 측에 필요한 저장 공간'으로 어떤 것을 사용할지 정의한다. 디폴트로 'MemoryStore'를 사용한다고 한다.

secret은 세션 ID 생성에 사용되어 이후에 세션 ID가 유효한지 판단하는 데 활용된다.

saveUninitialized: true를 설정하면 서버에서 req.session = ...와 같이 세션을 'initialize'하지 않아도 세션을 스토리지에 저장하고, 쿠키를 설정한다.

resave: true를 설정하면 세션에 변동사항이 없어도 매 요청마다 세션을 새로 저장한다. false로 설정하는 것이 일반적이며 스토리지에 따라 true를 줘야하는 경우가 있다고 하는데, 자세한 내용은 문서를 참고하면 될 것 같다.

cookie: { }에 설정하는 값들은 위에서 설명한 쿠키 attributes(MaxAge, HttpOnly 등)를 결정한다.

활용

간소화된 로그인 예제를 통해 세션이 실제로 어떻게 활용되는지 살펴보자.

 

// ...

app.get('/', (req, res, next) => {
  res.setHeader('Content-Type', 'text/html');
  res.write('<a href="/login">Login</a>');
  res.write('<br>');
  res.write('<a href="/mypage">My Page</a>');
  res.end();
});

app.get('/login', (req, res, next) => {
  // 로그인 과정(아이디, 비밀번호 검증 등) 생략 
  req.session.user = { username: 'sangin' };

  res.send('User Logged In');
});

app.get('/mypage', (req, res, next) => {
  console.log(req.session);

  let username;

  if(req.session.user) {
    username = req.session.user.username;
  } else {
    username = 'Not a user';
  }

  res.send(username);
});

// ...

 

'Hello World' 대신 링크 두 개를 추가했다. 클라이언트가 로그인 페이지로 이동하면 로그인이 되고, 'req.session'에 유저 정보를 저장한다. 마이페이지로 이동했을 때 유저의 정보가 세션에 있다면 유저의 이름을 응답한다.

반대로 로그인하지 않고 마이페이지를 방문하거나 Application 탭에서 쿠키를 삭제하고 마이페이지를 방문해보면 'Not a user'가 응답되는 것을 확인할 수 있다.

정리

세션이 무엇이고 어떤 식으로 클라이언트의 상태 정보를 유지하는지 알아보았다. 예시에서처럼 클라이언트 측에는 세션의 ID만 저장되기 때문에 보안상 이점이 있지만, 서버 측에 세션들을 관리할 별도의 저장 공간이 필요하다.