봄날은 갔다. 이제 그 정신으로 공부하자

node.js로 웹서비스 만들기 (5. 세션 관리) 본문

학습

node.js로 웹서비스 만들기 (5. 세션 관리)

길재의 그 정신으로 공부하자 2023. 7. 5. 11:11

 

개발 환경을 구성하고 DB연결하고 라우터 처리까지했으니 이제 세션 관리 기능을 개발해야 합니다.

힘드네요...

 

세션이란?

세션은 서버에 저장되는 key-value 형식의 데이터로 세션은 서버에서만 생성 할 수 있어 사용자의 로그인 상태를 확인할 때 사용 됩니다.

 

세션의 동작

서버측에서 세션을 생성할 땐, 클라언트에게 SID(session ID)를 발급합니다.

SID가 발급되면 connect.sid 쿠키가 생성된  것을 확인 할 수 있습니다.

서버측엔 해당 클라이언트가 사용할 세션을 저장합니다. 클라이언트에 SID 쿠키가 저장되어 있으니 클라이언트로부터 request가 오면 request의 쿠키로부터 SID를 확인 할 수 있으므로 해당 클라이언트의 세션을 매핑해 사용할 수 있습니다.

 

세션 사용 방법

Express-session 설치

node.js에서 세션을 사용하기 위해서는 우선 아래와 같이 express-session을 설치합니다.

> npm install express-session

 

미들웨어 추가

세션을 사용하기 위해서는 아래와 같이 미들웨어를 등록해야 합니다.

// index.js
const expressSession = require('express-session');

app.use(
  expressSession(
    {
      secret: "myKey", // [필수] SID를 생성할 때 사용되는 비밀키로 String or Array 사용 가능.
      resave: true, // true(default): 변경 사항이 없어도 세션을 다시 저장, false: 변경시에만 다시 저장
      saveUninitialized: true // true: 어떠한 데이터도 추가되거나 변경되지 않은 세션 설정 허용, false: 비허용
    })
);

 

미들웨어 옵션

  • cookie: ID 쿠키에 대한 설정으로 cookie 옵션은 객체 형식으로 줄 수 있으며 default는 { path: '/', httpOnly: true, secure: false, maxAge: null }입니다. 
  • path: String, 쿠키를 전송할 경로를 지정합니다. 세션의 경우 session을 사용하게 될 path를 정해주는 역할을 합니다. 정해진 path가 아닌 경로에 접근하면 session이 없는 것처럼 작동합니다. 
  • httpOnly: Boolean, 클라이언트 측에서 쿠키에 접근하는 것을 막습니다. 즉, document.cookie를 통한 쿠키 접근을 막습니다.
  • secure: Boolean, https 통신에서만 쿠키 전송을 허용합니다. express-session 개발자는 secure: true를 사용할 것을 권장. 
  • maxAge: 쿠키가 유지될 기간으로 초 단위로 설정할 수 있습니다. 세션을 통한 로그인 유지를 구현하려면 maxAge를 설정해야 함.
  • genid: SID로 사용할 로직을 정의하는 옵션으로 genid: function(req){}형식으로 사용하면 되고, SID로 사용할 String을 return 하면 됩니다. default로 uid-safe 라이브러리를 통한 SID 생성 함수를 사용합니다.
  • name: SID 쿠키의 이름을 설정합니다. default는 connect.sid입니다.
  • proxy: 리버스 프록시를 사용하는 경우 설정하는 옵션입니다. true로 설정하면 X-Forwarded-Proto 헤더가 사용됩니다. default는 undefined로, express에 trust proxy로 등록된 주소에 대해서만 세션이 동작합니다.
  • resave: express-session은 기본적으로 세션에 변경이 있을 때만 세션을 저장합니다. resave:true로 설정한다면 변경사항이 없어도 session을 다시 저장합니다. default는 true입니다. false로 설정하면 불필요한 session 저장을 막아주기 때문에 보통 false로 설정합니다. 
  • saveUninitialized: 세션이 생성되었지만 어떠한 데이터도 추가되거나 변경되지 않은 상태를 uninitialized 라고 합니다. saveUninitialized:true로 설정한다면 uninitialized session도 저장합니다. false로 설정하면 uninitialized session은 저장하지 않으므로 리소스 활용 측면에서 조금 더 유리합니다.
  • rolling세션이 만료되기 전, 새로 고침 또는 페이지 이동이 일어나면 세션 만료를 갱신하는 옵션입니다. default는 rolling:false

 

세션 메소드

session에서 사용 가능한 메소드는 다음과 같습니다.

  • regenerate(callback)세션을 재생성하는 메소드로 새로운 SID를 부여합니다.  사용 방법은 아래와 같습니다.
req.session.regenerate(function(err){
    // will have a new session here
})
  • destroy(callback)세션을 명시적으로 삭제하는 메소드로 사용 방법은 아래와 같습니다.
req.session.destroy(function(err){
    // cannot access session here
})
  • reload(callback)store에 저장된 session date를 다시 불러오는 메소드로 사용 방법은 아래와 같습니다.
req.session.reload(function(err){
    // session updated
})
  • save(callback): 세션을 store에 명시적으로 저장하는 메소드로 response의 마지막에 session을 자동으로 저장하기 때문에 보통은 사용하지 않아도 됩니다. 사용 방법은 아래와 같습니다.
req.session.save(function(err){
    // session saved
})
  • touch(callback)명시적으로 세션의 maxAge를 갱신하는 메소드로 rolling 옵션의 역할과 비슷한 기능을 수행하는 메소드로 사용 방법은 아래와 같습니다.
req.session.touch()

 

세션에 포함된 변수들

  • session.id, sessionIDsessionID는 변경 불가능한 값으로 session을 load하거나 생성할 때 부과되는 값 입니다. 세션이 갱신되면 이 값도 변경이 됩니다. req.session.id는 req.sessionID의 별칭(alias)입니다.
  • session.cookie: SID 쿠키의 설정값이 들어 있습니다. 이 값은 SID와 달리 수정할 수 있어서 사용자마다 다르게 쿠키를 설정해 줄 수 있습니다.

 

세션 예제

아래 코드는 간단한 세션 사용 예제 입니다.

우선 아래와 같이 필요한 것들을 설치합니다.

// package.json
{
  "name": "nodesession2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "bootstrap": "^5.3.0",
    "cookie-parser": "^1.4.6",
    "ejs": "^3.1.9",
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "nodemon": "^2.0.22",
    "serve-static": "^1.15.0"
  }
}

 

 

index.js 파일에 로그인 & 로그아웃 등 세션 관리 기능을 추가해줍니다.

세션 관리를 하는 index.js에 아래와 같이 root 라우터로 등록해 다른 라우터에서도 세션 기능을 사용할 수 있도록 합니다.

var router = express.Router();
...
app.use('/', router);

 

전체 소스는 아래와 같습니다.

// index.js
const express = require('express');
const serveStatic = require('serve-static');
const cookieParser = require('cookie-parser');
const expressSession = require('express-session');
const bodyParser = require('body-parser');
const path = require('path');

const app = express();
app.set("port", 3000);

// view engine 템플릿 사용을 명시
app.set('views' , path.join(__dirname, 'views'));
app.set('view engine' , 'ejs');

// 라우터 생성
var mainRouter = require('./routes/main');
var router = express.Router();


// 미들웨어를 등록한다.
app.use(serveStatic(path.join(__dirname, "public")));
app.use(bodyParser.json());
app.use(
  bodyParser.urlencoded({
    extended: false
  })
);

// cookie and session assign middleWare
app.use(cookieParser());

// 세션 세팅
app.use(
  expressSession(
    {
      secret: "myKey",
      resave: true,
      saveUninitialized: true,
    })
);

app.use('/', router);
app.use('/main', mainRouter);


router.get('/', function(req, res){
  console.log('[GET] root');
  if(req.session.user){
      res.redirect('/main/intro');
  } else {
      res.render('login');
  }
});

router.get('/login', function(req, res){
  console.log('[GET] auth login');
  res.render('login');
});

router.post('/login', function(req, res){
  console.log('[POST] auth login');
  
  const paramID = req.body.id || req.query.id;
  const pwd = req.body.password || req.query.password;
  console.log('  id: ' + paramID + ', password: ' + pwd);
  if(req.session.user){
    console.log('  이미 로그인 되어 있습니다.');
  } else {
    console.log('  사용자 정보 저장');
    // 세션에 유저가 없다면,
    req.session.user = {
      id: paramID,
      pwd: pwd,
      name: "MyName!!!",
      authorized: true,
    };
    res.redirect('/main');
    // res.render('intro');
  }
});

//router.route('/logout').get(function(req, res){
router.get('/logout', function(req, res){
  if(req.session){
    req.session.destroy(()=>{
        res.redirect('/');
    });
  }
});


app.listen(app.get("port"), () => {
  console.log(`${app.get("port")}에서 서버실행중.`);
});

 

 

// routes/main.js
const express = require('express');
const bodyParser = require('body-parser');

var router = express.Router();

// 미들웨어를 등록한다.
router.use(bodyParser.json());

router.get('/', function(req, res){
  console.log('[GET] main root');
  if(!req.session.user){
    console.log('  어라? 로그인이 안되어 있네? 로그인 화면으로...');
      res.render('login');
      return;
  }
  res.render('intro');
});

router.get('/intro', function(req, res){
  console.log('[GET] main intro');
  if(!req.session.user){
    console.log('  어라? 로그인이 안되어 있네? 로그인 화면으로...');
      res.render('login');
      return;
  }
  res.render('intro');
});

module.exports = router;

 

로그인 & 메인 화면은 아래와 같이 구현합니다.

// views/login.ejs
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
  <title>로그인</title>
</head>
<body>
  <h2>로그인</h2>
  <form method="post" action="/login">
    <div>
      <label>ID: </label>
      <input type="text" name="id"/>
    </div>
    <div>
      <label>PASSWORD: </label>
      <input type="text" name="password"/>
    </div>
    <div>
      <input type="submit" value="로그인"/>
    </div>
  </form>
</body>
</html>

 

// views/intro.ejs
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
  <title>Example</title>
</head>
<body>
  <p>로그인 되었습니다.</p>
  <a href="/logout">로그아웃</a>
</body>
</html>

 

 

주의 사항 (이라고 쓰고 내가 삽질한 기록이라고 읽음.)

간단한 샘플프로그램에서 세션 기능을 구현했을 때에는 잘 동작했는데 실제 내가 만드는 웹서비스에 세션 기능을 적용했을 때는 제대로 동작하지 않는 문제가 발생함.

원인을 파악하기 위해 여기저기 디버깅해보니 route 처리를 하면 root를 라우터로 등록해 다른 라우터에서도 세션 기능을 사용할 수 있도록 index.js에 아래 코드를 추가해주어야 하는데 이걸 빼먹은 것 이었음...

  -> app.use('/', router);

저거 한줄 때문에 엄청 고생함.

Comments