HTTP
HTTP는 stateless 하다. HTTP를 기반으로 통신하는 서버는 기본적으로 클라이언트에 대해 어떤 상태 정보도 유지하지 않는다는 뜻이다. 어떤 요청이 들어왔을 때 해당 클라이언트가 이전에 요청을 보냈던 클라이언트인지, 몇 번째로 요청을 보내고 있는 것인지 등을 알 수 없다. 그렇기 때문에 클라이언트의 인증 상태(로그인한 사용자인지 여부 등)를 판단하려면 추가적인 조치가 필요하다. 여기서 등장하는 것이 쿠키와 세션이다.
쿠키
아래는 IETF RFC와 MDN을 참고 및 요약, 정리한 내용이다. 보다 상세한 설명을 보려면 링크를 참고하면 될 것 같다.
개념
쿠키란 간단히 말해 '작은 데이터 조각'이다. 서버가 클라이언트에게 응답을 보낼 때 '쿠키를 설정하겠다'는 의미의 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
위의 예시처럼 쿠키를 설정하면 해당 쿠키는 브라우저가 종료될 때 삭제된다. 만약 쿠키를 일정 기간 동안 클라이언트의 디바이스에 유지시키고 싶다면 Expires
나 Max-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 스펙이 아니라 서버 어플리케이션에서 사용되는 개념 또는 방식이기 때문에 구현 방법은 다양할 수 있다. 여기서는 express와 express-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만 저장되기 때문에 보안상 이점이 있지만, 서버 측에 세션들을 관리할 별도의 저장 공간이 필요하다.
'개발' 카테고리의 다른 글
Gatsby 블로그: Pagination 추가하기 (0) | 2022.11.30 |
---|---|
Gatsby 블로그: 포스트에 카테고리 추가하기 (0) | 2022.11.30 |
Gatsby로 블로그 만들기 (0) | 2022.11.30 |
JavaScript 클로저 (0) | 2022.11.30 |
JavaScript 실행 컨텍스트와 관련 개념들 (0) | 2022.11.30 |