티스토리 뷰

반응형

로그인의 기본인  쿠키/세션을 nodejs에서 구현해 보려한다. JWT 방식도 있지만, 현업에서 가장 많이 사용하는 세션방식에 대해 알아 볼 것이다. 쿠키와 세션의 가장 큰 특징은, 식별을 도와주는 세션 id를 브라우저에 보내기 위해서는 쿠키에 담아서 보내야 한다는 점이다. 즉, 둘 중 하나를 선택하는 것이 아닌, 쿠키에 담에서 세션에 전달한다는 것을 먼저 알아야 한다.

 

 

아래의 코드만 따라한다면, login에 session을 넣을 수 있다. 추가로 다른 실무 내용을 확인하려면 아래의 url을 들어가보자.

[Web/nodejs] - nodejs 기초 총 정리

 

 

https://upload.wikimedia.org/wikipedia/commons/b/b4/Choco_chip_cookie.png

 

 

쿠키

: client의 상태 정보를 client의 컴퓨터 메모리에 저장한다. 즉, 서버가 client에게 보내는 데이터의 조각이라고 할 수 있다. client(브라우저)는 받은 쿠키를 저장했다가 동일 서버에 재요청 시 자동으로 쿠키를 함께 보낸다.

: 쿠키는 중간에 탈취당할 가능성이 있어 안전하지 않기 때문에, 사용자의 정보는 서버에 저장하고 쿠키에는 식별자(connect.sic나 sessionid)만 넘겨주는 경우가 많다. 서버에 저장된 사용자의 정보는 세션으로 관리된다.

: 쿠키는 브라우저에 저장되는 반면 세션은 서버에 저장된다. 

: 쿠키는 유효기간을 정할 수 있지만, 세션은 보통 브라우저를 닫으면 사라진다.

: 서버에서 한 번 쿠키값을 보내게 되면, 그 후에는 client가 서버에 요청 시 자동으로 쿠키를 req에 포함해서 보내게 된다.

ex_ header의 Set-cookie에 user name을 포함시켜 보낸다.

 

 

세션

: client의 요청 시 정보를 client의 메모리가 아닌, 웹 서버 내부에 세션 아이디 파일을 만들어 서비스가 돌아가고 있는 서버에 저장을 하는 것이다.

: 쿠키와 동일하게 상태 정보를 저장하지만, 외부에서 세션 정보를 열람해도 개인 정보와 매칭이 불가능하다. 즉, 쿠키와 가장 큰 차이점은 유출되어도 역으로 확인이 불가능하다는 것이다.

ex_ header의 Set-cookie에 user name을 포함시켜 보내면 보안의 위험이 있으니, user 정보가 아닌  다른 값을 넣어보낸다.

 

 

정리

  1. client가 첫 접속 시, 서버는 식별자로 response-header field인 set-cookie에 session-id(임의의 긴 문자열)을 담아서 보낸다.
  2. 서버에서 준 session-id는 서버와 client(브라우저) 메모리에 둘다 저장된다. 이 때 쿠키 이름은 JSESSIONID 이다.
  3. client가 서버 에 접속 시, 서버는 request-header field인 cookie를 확인해 session-id를 확인한다.

 

 

쿠키 설정은 아래와 같다. response인 res에 넣는다면 setHeader에 넣어준다.

res.setHeader('Set-Cookie', 
  ['a-cookie=apple', 's-cookie=strawberry']
);
  • Set-Cookie 값으로 위와 같이 배열을 주면, 여러 개의 쿠키를 할당 할 수 있다.

 

 

아래와 같이 Expires나 Max-Age를 유효한 기간을 정해 줄 수 있다.

 

res.setHeader('Set-Cookie', 
  [`a-cookie=apple; Max-Age=${60*60*24}`]
);
  • [`a-cookie=apple; Max-Age=${60*60*24}`] : 여기서 주의 할 점은 따옴표(')가 아닌 백틱(`)을 사용해야 하고 세미클론(;) 뒤에 Max-Age나 Expires를 적어준다.  Max-Age는 초를 기준으로 적기 때문에 60*60*24이면 하루를 나타낸다.
  • Max-Age : 쿠키가 얼마 동안 유지되는가를 설정한다.
  • Expires : 쿠카가 언제까지 유지될지를 설정하는 것이다.

 

 

쿠키 접근은 아래와 같이 콘솔에서 접근이 가능하다.

 

 

그렇다면, 이러한 상식으로 사용자의 정보를 훔쳐보는 것이 가능하다는 것이다. 이러한 위험성을 방지하기 위해 HttpOnly나 Secure를 사용한다. 사용법은 아래와 같이 간단하다.

 

res.setHeader('Set-Cookie', 
  ['a-cookie=apple; HttpOnly', 's-cookie=strawberry; HttpOnly; Secure']
);
  • HttpOnly : 쿠키가 웹브라우저와 서버가 통신할 때만 쿠키를 발행한다. document.cookie로 콘솔창에서 출력이 안된다.
  • Secure : HTTP가 아닌 HTTPS일 때만 쿠키를 전송한다. HTTP에서는 쿠키가 포함되지 않는다.
  • Path : path 옵션에 적은 브라우저가 접근했을 때만 서버에서 Path 옵션이 붙은 쿠키를 넘겨준다.

 

위의 내용은 일반적인 내용이고, express에서는 setHeader가 아니라 req.cookie로 아래와 같이 담는다.

res.cookie('name', 'tobi', { domain: '.example.com', path: '/admin', secure: true });
  • name=tobi 라는 쿠키가 생성된다
  •  .example.com 도메인 아래 /admin path이고 HTTPS 통신인 상황에서만 브라우저에서 서버로 이 쿠키를 다시 재전송한다.

 

: 쿠키에 id, pw를 넣으면 안 된다.

: 인증은 세션으로 하고, 쿠키에는 사용자의 식별자만 저장한다.

: 사용자의 정보를 암호화 하는데 pdkdf2, bcrypt 들의 암호화 라이브러리를 사용한다.

 

 

 

 

세션 저장에는 여러 방법이 있다. 보통 session은 서버 메모리(MemoryStore)에 저장되지만, 서버가 중단되면 세션은 모두 초기화 되기 떄문에 다른 저장소에 저장할 필요가 있다. session-file-store 모듈을 사용하여, FileStore에 저장한다. 아니면 express-mysql-session 모듈을 사용하여 DB에 저장한다. 아니면 Redis를 사용하여 저장하는 것도 가능하다. 관련 내용은 여기를 참고해서 고르자.

 

express-session로 우선 구현을 해보자.

 

express-session 이용하기

$ npm install express-session

 

설정을 해보자.

 

 

const session = require("express-session"); //모듈 가져오기
app.use( // 미들웨어 적용
  session({
    httpOnly: true,
    secure: ture,
    secret: "@haAdvanced",
    resave: false,
    saveUninitialized: true,
    cookie: {
    	httpOnly: true,
        secure: true
    }
  })
);
  • httpOnly - 자바스크립트로 쿠키 조회 안되게 한다.
  • secure - https 환경에서만 session 정보를 주고 받는다. Node 서버가 프록시 뒤에 있다면 app.use(session({}))을 하기 전에 app.set('trust proxy', 1)을 설정해주는 게 필요하다고 한다. 기본값은 false지만 true르 ㄹ권장한다.
  • secret – 쿠키를 임의로 변조하는것을 방지하기 위한 값이며, 이 값을 토대로 세션을 암호화 하여 저장한다. 하나의 문자열을 받을 수도 있고 여러 배열을 받을 수도 있다. 배열인 경우 첫 번째 요소만 세션 ID 쿠키에 쓰인다.
  • resave – 세션에 변경사항이 없어도 항상 저장할 지 설정하는 값이다. 기본값은 true인데, 보통은 false를 많이 쓴다.
  • saveUninitialized – 세션이 최초 만들어지고 수정이 안된다면, 저장되기 전에 uninitialized 상태로 미리 만들어서 저장한다. 보통 false를 많이 쓴다.
  • cookie - 세션 ID 쿠키 객체를 설정한다. 기본값은 path: '/', httpOnly: true, secure: false, maxAge: null 이다. 위에서 본 것과 같이 domain, expires, httpOnly 등 쿠키의 옵션을 설정해줄 수 있다.

 

express-session을 사용하면, 세션은 서버 메모리에 저장된다고 적었다. 즉, 서버꺼지면 모두 초기화 된다고 보면된다. 먼저 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,	
  secret: 'secret key',	
  resave: false,
  saveUninitialized: true,
  cookie: {	
    httpOnly: true,
    secure: true
  },
  store: new fileStore()
}));

 

이런식으로 추가를 해주면 된다. 그러 후에 서버를 올리면 아래와 같이 sessions 디렉토리가 생긴다. 

 

 

관련해서 로그인, 회원가입, 로그인 확인 로직을 만들어 보겠다. 아래와 같다.

 

우선은 app.js에서 간단히 아래와 같이 만들었다.

 

~
var express = require('express');

...


var app = express();


// express-session setup
const session = require("express-session")

// session-file-store
const fileStore = require("session-file-store")(session);
app.use(
  session({
    secret: "@haAdvanced",
    resave: false,
    saveUninitialized: true,
    store: new fileStore(),
  })
)



// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');



app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

~~

 

 

아래에서 mysql에서 데이터를 가지고 오는 로직을 mybatis로 구현을 했다. 그러나 단순히 query 부분에 SQL을 적기만 해도 된다. mysql연동 관련 내용은 여기를 눌러서 확인 할 수 있다.

 

// routes/users.js
var express = require('express');
var router = express.Router();

var mysql = require('../config/mysql/db');
var mysql2 = require('../config/mysql/db2');

const mybatisMapper = require('mybatis-mapper');
mybatisMapper.createMapper(['./models/mybatis/users/usersMapper.xml'])
var format = {language: 'sql', indent: ' '}

const crypto = require('crypto');

/*
{
    "userId": "admin",
    "userPwd": "admin",
    "userName": "king",
    "authType": "a"
}

*/
router.post('/sign_up', function(req, res, next){
  var body = req.body;
  var inputPassword = body.userPwd;
  var salt = Math.round((new Date().valueOf() * Math.random())) + "";
  let hashPassword = crypto.createHash("sha512").update(inputPassword + salt).digest("hex");
  console.log(hashPassword)

  var param = {
    userId : body.userId,
    userPwd : hashPassword,
    userName : body.userName?body.userName:null,
    authType : body.authType?body.authType:'y',
    salt : salt,
  }
  var query = mybatisMapper.getStatement('userMapper', 'insertUserQuery', param, format)
  mysql.query(query, (error, rows) => {
    res.json(rows)
  })
})

router.post('/login', async function(req, res, next){
  let body = req.body;
  var param = {
    userId : body.userId,
  }
  var query = mybatisMapper.getStatement('userMapper', 'findUserQuery', param, format)
  let [result, fields] = await mysql2.query(query)
  let dbPassword = result[0].user_pwd;
  let inputPassword = body.userPwd;
  let salt = result[0].salt;
  let hashPassword = crypto.createHash("sha512").update(inputPassword + salt).digest("hex");

  if(dbPassword === hashPassword){
    console.log("-------------success login");
    req.session.userId = body.userId;
    req.session.is_logined = true;
  } else {
    console.log("-------------fail login")
  }
  res.send('success login')
})


router.get("/logout", function(req, res, next){
  req.session.destroy();  // 내부 sessions 폴터 캐쉬 삭제
  res.clearCookie('sid')
  res.send('logout')
})

router.get('/check', (req, res, next) => {
  if(req.session.is_logined){
    return res.json({message: 'user 있다'});
  }else{
    return res.json({message: 'user 없음'});
  }
});

module.exports = router;

 

 

// config/mysql/db.js
/* mysql setting */
const mysql = require('mysql');
const connection = mysql.createConnection({
  host : '127.0.0.1',
  port : '3306',
  user : 'root',
  password : 'root',
  database : 'mediation_server'
});

module.exports = connection;

 

// db2
var mysql2 = require("mysql2")
var pool  = mysql2.createPool({
  host : '127.0.0.1',
  user : 'root',
  password : 'root',
  database : 'mediation_server'
});
var promisePool = pool.promise();
module.exports = promisePool;

mysql2를 사용하는 이유는 mysql에서 data를 가져올 때 비동기 로직에서 

let [result, fields] = awaait mysql2.query(SELECT * FROM user WHERE user_id=#{id}) 

와 같은 로직을 작성하여 받은 데이터를 변수로 넣기 위해서 사용한다. 

 

 

회원가입을 아래와 같이 해보자

 

 

그러면 mysql db에는 아래와 같이 값이 잘 들어간다.

 

 

이제 아래와 같이 로그인을 해보자

 

로그인 을 하면 session 폴더 아래 json파일이 생성된다.

 

{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userId":"hanpy","is_logined":true,"__lastAccess":1638324608810}

 

 

 

로그인이 됐는가 확인을 하는 방법은 아래와 같다.

 

 

그러면, logout을 해보자.

 

 

로그아웃을 하면 sessions 내부의 파일이 아래와 같이 지워진다.

 

 

 

다시 check를 해보자.

 

user가 없다고 잘 뜨는 것을 확인 할 수 있다.

 

 

사실 passport를 활용해서 하는 방식도 많이 사용하긴 하지만, 이 방법이 난 더 쉬워 보인다.

 

조만간 redis에 token 넣는 방법과 react에 붙이는 방법도 적어봐야겠다.

 

 

 

 

 

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함