먹고 기도하고 코딩하라

[Node.js] express-session 다뤄보기 본문

Javascript

[Node.js] express-session 다뤄보기

2G Dev 2020. 7. 8. 09:12
728x90
728x90

 

HTTP 쿠키와 Node.js, express에서의 쿠키 사용

세션을 다뤄보기 전에 먼저 HTTP 쿠키에 대해 간단히 설명할까 한다.

쿠키는 HTTP 프로토콜에 포함된 기술로, 서버가 사용자의 브라우저(클라이언트)에 전송하는 데이터 조각이다. 브라우저는 그것을 저장해뒀다가 동일 서버에 재요청 시 쿠키를 함께 보내게 된다. 로그인 상태가 유지되고 쇼핑몰에서 장바구니가 유지되는 것이 다 쿠키 덕분이다.

하지만 쿠키는 중간에 탈취당할 수도 있기 때문에 그리 안전하지는 않다. 그래서 서버에 사용자의 정보를 저장하고 쿠키에는 식별자(cid나 session id)만 넘겨주는 경우가 많다. 이렇게 서버에 저장된 사용자의 정보는 세션으로 관리된다.

쿠키와 세션의 차이점은 쿠키는 브라우저에 저장되는 반면 세션은 서버에 저장되고, 쿠키는 MaxAge를 두거나 Permanent를 지정할 수 있는 반면 세션은 보통 사용자가 서버와의 접속을 끊고 브라우저를 닫으면 사라진다는 점 등이 있다.

 

Node.js에서의 쿠키 사용

어쨌든 세션도 세션 아이디를 브라우저에 보내야 식별이 가능하기 때문에 쿠키를 사용한다. 쿠키는 다음과 같이 설정할 수 있다.

res.writeHead(200, {
  'Set-Cookie': [
    'yummy-cookie=choco',
    'tasty_cookie=strawberry'
  ],
  'Content-Type': 'text/html'
});

 

이렇게 writeHead로도 보낼 수 있고, setHeader로 묶어 보낼 수도 있다.

res.setHeader('Set-Cookie', 
  ['yummy-cookie=choco', 'favorite-cookie=strawberry']
);

 

만약 writeHead와 setHeader를 따로 써준다면 setHeader가 writeHead보다 앞에 와야 한다. 위의 예처럼 Set-Cookie의 값으로 배열을 주게 되면 Set-Cookie에 여러 개의 쿠키를 할당할 수 있다. 이 때 배열 요소는 쿠키 key=쿠키 value, 쿠키 key=쿠키 value 식이다. 브라우저는 이제 이 서버에 다시 접속할 때 Cookie: yummy-cookie: choco; favorite-cookie: strawberry 라는 정보를 서버에 전송하게 된다.

서버에서 한 번 쿠키 값을 보내게 되면 그 뒤로 req는 서버에 요청할 때 자동으로 쿠키를 보내게 된다. 기본적으로 Set-Cookie를 하게 되면 Session 쿠키(브라우저를 끄면 사라지는 쿠키)인데, 웹브라우저가 켜져 있는 동안만 유효하다. 쿠키에 Max-Age나 Expires를 주면 쿠키가 얼마나 살아남을 것인지 정할 수 있다. Expires는 쿠키가 언제 죽을 것인가이고 Max-Age는 쿠키가 얼마 동안 살 것인가를 정하는 것이다. 개인적으로는 날짜를 계산하는 것보다 그냥 7일이면 7일, 30일이면 30일 하는 식으로 계산하는 게 더 편해서 Max-Age를 좋아한다. Max-Age는 초 단위로 설정하므로 아래 예제는 24시간 동안 살아남는 yummy-cookie 쿠키를 만들 수 있다.

쿠키의 키와 값을 적어준 다음 세미콜론(;)을 찍어주고 Max-Age=값 을 적어준다.

res.setHeader('Set-Cookie', 
  [`yummy-cookie=choco; Max-Age=${60*60*24}`, 'favorite-cookie=strawberry']
);

 

그런데 쿠키는 브라우저 콘솔에서 접근이 가능하다. document.cookie로 접근하면 쿠키를 읽을 수 있는데, 만약 중요한 정보를 담고 있다면 약간의 스크립트로도 사용자의 정보를 훔쳐볼 수도 있을 것이다. 그런 사태를 대비하기 위해 쿠키를 지키는 옵션이 몇 가지 있는데, 대표적인 것이 HttpOnly와 Secure이다.

HttpOnly는 쿠키가 웹브라우저와 서버가 통신할 때만 쿠키를 발행한다. 이게 무슨 말이냐면 브라우저에서 콘솔 띄우고 document.cookie 하면 HttpOnly 옵션이 걸린 쿠키는 출력되지 않는다는 말이다. 자바스크립트로 출력해서 털리면 안 되는 정보는 HttpOnly 필요하다. yummy-cookie에 HttpOnly 옵션을 붙여보자.

res.setHeader('Set-Cookie', 
  [`yummy-cookie=choco; HttpOnly`, 'favorite-cookie=strawberry']
);

 

Secure은 서버와 브라우저가 HTTP가 아닌 HTTPS를 통해 통신할 경우에만 쿠키를 전송하는 것이다. 이번엔 favorite-cookie를 HttpOnly & Secure 쿠키로 만들어보자.

res.setHeader('Set-Cookie', 
  [`yummy-cookie=choco; HttpOnly`, 'favorite-cookie=strawberry; HttpOnly; Secure']
);

 

브라우저가 나중에 HTTP 환경에서 접속했을 때 서버로 favorite-cookie 쿠키는 보내지 않는다. 

Path와 Domain 옵션도 있다. Path 옵션을 붙이면 해당 path로 브라우저가 접근했을 때만 서버에 Path 옵션 붙은 쿠키를 넘겨준다. 이것은 지금 접속하고 있는 것이 해당 웹서버가 맞는지 확인하고 쿠키를 보내기 때문에 혹시라도 공격이 일어나 다른 사이트로 리다이렉션됐을 때는 쿠키가 전송되지 않는다. 자세한 사항은 MDN 문서를 확인하기 바란다. 이외에도 SameSite라는 CSRF를 예방할 수 있는 옵션이 있다는데 현재 실험 중이라 써먹기엔 좀 그렇다.

 

express에서 쿠키 접근

참고로 express에서 쓰는 response 객체에는 writeHead라는 메소드가 없다. 이건 express를 사용하지 않았을 때 쿠키를 보내는 방식이고 express일 때는 req.cookie를 사용하게 된다. 다음은 express docs에 나와 있는 req.cookie의 예제이다.

res.cookie('name', 'tobi', { domain: '.example.com', path: '/admin', secure: true });

 

여기서는 name=tobi 라는 쿠키가 생성되는데 .example.com 도메인 아래 /admin path이고 HTTPS 통신인 상황에서만 브라우저에서 서버로 이 쿠키를 다시 재전송하게 된다. 

 

그러나 근본적으로는 쿠키에 아이디, 비밀번호를 넣으면 절대 안 된다. 인증은 세션으로 하고 사용자의 식별자만 쿠키에 저장하는 것이 가장 안전한 방법이다. 사용자의 정보를 암호화하는 데는 pbkdf2, bcrypt 등의 암호화 라이브러리를 사용할 수 있다고 한다.

 

 

express-session 

세션은 쿠키처럼 사용자의 정보를 그대로 값으로 가지지 않고 cid나 sessionid만을 쿠키에 넣고 해시값으로 암호화를 하기 때문에 탈취해도 값을 알아내기가 어렵다. 그러니 세션을 써보자.

다음과 같은 명령어로 express-session을 설치할 수 있다.

npm i express-session
const express = require('express');
const app = express();
const session = require('express-session');	//세션관리용 미들웨어

app.use(session({
  httpOnly: true,	//자바스크립트를 통해 세션 쿠키를 사용할 수 없도록 함
  secure: ture,	//https 환경에서만 session 정보를 주고받도록처리
  secret: 'secret key',	//암호화하는 데 쓰일 키
  resave: false,	//세션을 언제나 저장할지 설정함
  saveUninitialized: true,	//세션이 저장되기 전 uninitialized 상태로 미리 만들어 저장
  cookie: {	//세션 쿠키 설정 (세션 관리 시 클라이언트에 보내는 쿠키)
    httpOnly: true,
    Secure: true
  }
}));

 

세션은 서버 메모리(MemoryStore)에 저장된다. 말인즉슨 서버가 한 번 내려가면 모두 초기화돼서 없어진다는 뜻이다. 그래서 세션을 저장할 저장소를 따로 지정할 수 있는데, 실제 서비스 배포 시에는 데이터베이스를 연결해서 세션을 유지하면 좋다. 보통 Redis를 사용한다고 한다. 뭘 사용할지에 따라서 store를 어떤 걸 설치해야 될지도 다르다. 가령 mySQL에 저장할 거라면 express-mysql-session이 적당해보이고, Redis나 MongoDB에 저장할 거라면 fortune-session이 괜찮아 보인다. API 문서를 보고 고르면 된다.

여기서는 일단 session file store로 설치해 보겠다.

npm i session-file-store
const express = require('express');
const app = express();
const session = require('express-session');	//세션관리용 미들웨어
const fileStore = require('session-file-store')(session);

app.use(session({
  httpOnly: true,	//자바스크립트를 통해 세션 쿠키를 사용할 수 없도록 함
  secure: ture,	//https 환경에서만 session 정보를 주고받도록 처리
  secret: 'secret key',	//암호화하는 데 쓰일 키
  resave: false,	//세션을 언제나 저장할지 설정함
  saveUninitialized: true,	//세션이 저장되기 전 uninitialized 상태로 미리 만들어 저장
  cookie: {	//세션 쿠키 설정 (세션 관리 시 클라이언트에 보내는 쿠키)
    httpOnly: true,
    secure: true
  },
  store: new fileStore()
}));

 

이렇게 하고 나서 처음 서버를 올리면 sessions 디렉토리가 생긴다. 사용자가 서버에 접속할 때 이 sessions 디렉토리 아래 세션 파일이 생긴다. 

app.use로 session 미들웨어를 쓰면 되는데, session 미들웨어는 매개변수로 객체를 하나 받는다. 이 객체 안에는 secret, resave, saveUninitialized, cookie, store 등 여러 가지 옵션이 있다.

  • secret의 경우 하나의 문자열을 받을 수도 있고, 여러 secret들의 배열을 받을 수도 있다. 만약 배열이 주어질 경우 첫 번째 요소만 세션 ID 쿠키에 sign하는 데 쓰인다고 한다. 
  • resave는 세션이 요청 중 변경되지 않아도 저장할지 말지를 저장한다. 기본값은 true인데, 보통은 false를 많이 쓴다고 한다.
  • saveUninitialized는 세션이 최초로 만들어져 수정되지 않았을 때 uninitialized로 저장되도록 하는 것이다. 기본값은 true인데 false를 많이 쓴다고 한다. 
  • cookie는 세션 ID 쿠키 객체를 설정한다. 기본값은 path: '/', httpOnly: true, secure: false, maxAge: null 이다. 위에서 본 것과 같이 domain, expires, httpOnly 등 쿠키의 옵션을 설정해줄 수 있다.
  • seucre은 false지만 true를 권장하고, Node 서버가 프록시 뒤에 있다면 app.use(session({}))을 하기 전에 app.set('trust proxy', 1)을 설정해주는 게 필요하다고 한다.

또 여기엔 없지만 name을 주면 세션 ID 쿠키 이름을 connect.sid가 아닌 다른 이름으로 바꿔줄 수 있는 등 여러 가지 옵션이 있으니 궁금하다면 API 문서를 잘 살펴보기 바란다.

 

express 앱에서 session 접근하기

앱에서는 이 세션 객체에 req.session으로 접근이 가능하다. 가령 사용자가 이 페이지에 몇 번이나 들어왔는지 보여주려면 다음과 같은 앱을 구성할 수도 있다.

app.get('/', (req, res, next) => {
  if (req.session.num === undefined) 
    req.session.num = 1;
  else
    req.session.num += 1;
  res.send(`${req.session.num}번 접속`);
});

 

같은 세션을 유지하고 있는 한 루트 path로 접속을 할 때마다 req.session.num이 1씩 늘어나게 된다.

이런 식으로 req.session.is_logined를 하면 생활코딩에서 만들었던 것처럼 로그인을 했는지 안 했는지 구현할 수도 있다. 

세션 객체를 없애고 싶다면(가령 로그아웃을 해서 세션을 유지할 필요가 없어진다면) req.session.destroy를 하면 된다.

req.session.destroy(err => {
  if (err) throw err;
  res.redirect(302, '/');
});

 

만약 세션 스토어가 바쁠 때 리다이렉션을 하면 바로 결과가 반영되지 않을 수도 있다. 그럴 때는 req.session.save(err => {})를 하면 된다. req.session.save를 실행하면 즉시 session 데이터 저장을 하고, err를 매개변수로 갖는 콜백 함수는 save가 모두 끝나면 작업할 내용들을 담고 있다.

if (email === authData.email && pwd === authData.pwd) {
  req.session.is_logined = true;
  req.session.nickname = authData.nickname;
  req.session.save(err => {
    if (err) throw err;
    res.redirect(302, '/');
  });
} else {
  res.end("Who?");
}

 

세션도 완벽해보이긴 하지만, 세션을 서버에 저장한다고 치면 동시접속자가 많아질 때 서버에 그만큼 세션이 많이 쌓이게 되므로 과부하가 올 수 있다는 단점이 있다.

또한 세션 하이재킹이라고 세션을 인증하기 위한 세션 id 자체를 빼앗는 세션 가로채기 공격도 있다. 예를 들면 홈페이지 관리자의 세션 아이디를 탈취해서 쿠키값을 관리자의 세션 아이디로 변경하는 안 좋은 상황이 생길 수도 있다. 이 경우에는 세션 id를 뺏으면 ID와 암호를 몰라도 로그인이 가능하기 때문에 위험하다.

이것을 예방하기 위해서는 세션에 로그인했을 때의 IP 값을 저장하고, 페이지를 이동할 때마다 현재 IP와 세션의 IP/브라우저 정보가 같은지 검사하는 방법이 있다고 한다.

 

 

 

Reference

생활코딩 - 세션과 인증

npm - express-session

 

 

728x90
반응형
0 Comments
댓글쓰기 폼