2019. 07. 21 수정


이번 글에서는 sequelize를 이용하여 회원 가입 기능을 구현해보고, crypto 모듈을 이용하여 비밀번호를 암호화하는 방법에 대해서 알아보도록 하겠습니다.

  • 개발환경

    • express-generator 4.16.1

    • MySQL 8.0.16

    • sequelize 5.10.1

예제를 진행하시려면, 셋팅은 여기를 참고하여 "1) 프로젝트 생성 및 npm 설치 ~ 3) sync 작성"를 진행해주시면 됩니다.




1. 회원 모델 정의하기

회원 가입을 하기 위해, 먼저 회원 모델을 정의해야 합니다.

sequelize-cli로 모델을 생성하고 이 때 생성된 migration 파일도 수정하겠습니다.

# sequelize model:create --name user --attributes "name:string, email:string, password:string, salt:string"


생성된 user 모델에서 제약조건들을 추가합니다.

/models/user.js

'use strict';
module.exports = (sequelize, DataTypes) => {
var user = sequelize.define('user', {
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isEmail: true
},
primaryKey: true
},
password: {
type: DataTypes.STRING,
allowNull: false
},
salt:{
type: DataTypes.STRING
}
});

return user;
};
  • email 컬럼을 고유키로 두고, email 양식이 맞는지 확인하는 validate도 추가하였습니다.
    • 즉, 데이터가 " foo@example.com "과 같은 이메일 형식이 아니면, 유효성 검사에 실패하여 오류 메시지를 출력하고 DB에 저장하지 않습니다.
    • Sequelize validate 옵션에 대한 설명은 여기를 참고해주세요.
  • salt 컬럼은 암호화에 필요한 것인데, 뒤에서 살펴보도록 하겠습니다.



이어서 migration 파일을 수정합니다.

/migration/create-user.js

'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('users', {
name: {
type: Sequelize.STRING,
allowNull: false
},
email: {
type: Sequelize.STRING,
allowNull: false,
validate: {
isEmail: true
},
primaryKey: true
},
password: {
type: Sequelize.STRING,
allowNull: false
},
salt:{
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('users');
}
};

migrations 파일까지 수정했으니, DB에 반영하기 위해 migrate 명령어를 실행하고, 서버를 실행합니다.

# sequelize db:migrate

# npm start





2. 회원 가입 form 만들기

다음으로 데이터를 받기 위해 회원 가입 뷰 페이지를 만들겠습니다.


/views/user/signup.ejs

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h3>회원 가입</h3>
<form action="/user/sign_up" method="post">
<table>
<tr>
<td>이름 : </td>
<td><input type="text" name="userName"></td>
</tr>
<tr>
<td>이메일 : </td>
<td><input type="text" name="userEmail"></td>
</tr>
<tr>
<td>비번 : </td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="회원가입"></td>
</tr>
</table>
</form>
</body>
</html>





3. 라우터 등록하기

express-generator로 프로젝트를 생성하면 /routes 폴더에 user.js 파일이 있습니다.

/routes/user.js 파일에 회원과 관련된 라우터 미들웨어를 작성하면, 회원과 관련된 내용만 한 파일에 있으므로 관리하기가 쉬워집니다.


아래의 두 라우터 함수를 작성합니다.

  • 회원 가입 뷰 페이지를 응답하는 GET 방식
  • "회원가입"버튼을 클릭했을 때 처리하는 POST 방식

/routes/user.js

const express = require('express');
const router = express.Router();
const models = require("../models");


router.get('/sign_up', function(req, res, next) {
res.render("user/signup");
});


router.post("/sign_up", function(req,res,next){
let body = req.body;

models.user.create({
name: body.userName,
email: body.userEmail,
password: body.password
})
.then( result => {
res.redirect("/users/sign_up");
})
.catch( err => {
console.log(err)
})
})

module.exports = router;


이제 회원 가입을 위한 모든 준비가 끝났습니다.

게시판을 만드는 것과 크게 다른 것이 없습니다.

브라우저에서 http://localhost:3000/users/sign_up 으로 요청하여, 회원 가입이 잘되는지 테스트를 해봅니다.





4. crypto 내장 모듈 소개

그런데 MySQL의 users 테이블을 확인해보면, 아래와 같이 password가 그대로 노출이 되었습니다.


회원의 개인정보인 비밀번호는 관리자를 포함하여 누구든 볼 수 있으면 안됩니다.

따라서 회원가입시, DB에 저장하는 순간 비밀번호를 암호화하여 누구도 확인할 수 없도록 해야 합니다.


Node.js에서는 비밀번호를 암호화 할 수 있도록 도와주는 내장 모듈인 crypto를 제공합니다. ( 참고문서 )


crypto는 해시를 생성하고 각종 암호화를 지원하는 유틸리티를 제공합니다.

해시는 복호화( 본래의 값을 복원할 수 없는 )가 불가능한 단방향 암호화 방식입니다.


그런데 로그인을 할 때, 비밀번호를 확인하려면 "DB에 저장된 password를 복호화해서 사용자가 입력한 평문의 값과 비교해야 하지 않을까?"하고 생각할 수 있습니다. 

그러나 그렇지 않습니다.

로그인 시, 사용자가 입력한 평문의 값을 똑같이 암호화 해서 DB에 저장된 암호화된 데이터와 비교해서 일치하면, 인증이 되는 방식으로 처리할 수 있기 때문에 암호화에 단방향 방식을 사용할 수 있습니다.


그런데 해시만 사용하면 해커가 해시 정보를 갖고 있는 레인보우 테이블을 이용해 원래의 값을 알아낼 수도 있습니다.

( 해시는 특정 포맷을 가진 문자열로 변환해주는 것이지, 엄밀히 말해서 암호화가 아닙니다. )

때문에 salt( 소금 )을 추가로 적용하여 보안성을 더 높여야 암호화가 잘 되었다고 할 수 있습니다.


salt는 랜덤 값으로써, salt 값을 추가하여 해시를 수 만번 반복하여 사용자의 비밀번호를 변형시킵니다.

따라서 salt를 추가하는 것만으로도 보안을 강화할 수 있습니다.





5. crpyto를 이용한 비밀번호 암호화

이제 회원가입 시, 라우터 함수에서 DB에 저장할 때 crypt 모듈을 사용하여 비밀번호를 암호화 할 수 있도록 수정하겠습니다.

crypto 모듈은 내장모듈이기 때문에 npm 설치는 필요없고, 상단에 require만 해주면 됩니다.

/routes/users.js

const crypto = require('crypto');

...

router.post("/sign_up", async function(req,res,next){
let body = req.body;

let inputPassword = body.password;
let salt = Math.round((new Date().valueOf() * Math.random())) + "";
let hashPassword = crypto.createHash("sha512").update(inputPassword + salt).digest("hex");

let result = models.user.create({
name: body.userName,
email: body.userEmail,
password: hashPassword,
salt: salt
})

res.redirect("/user/sign_up");
})

salt 값은 현재 시간에 랜덤 값을 곱해서 생성된 문자열로 생성했습니다.

그리고 이 값은 users 테이블의 salt 컬럼에 저장됩니다.


crypt 모듈을 사용하여 해시된 비밀번호 hashPassword가 생성되는 과정은 다음과 같습니다.

  • crypto 모듈 객체의 createhash() 메서드의 인자로 해시 알고리즘을 넘겨줍니다.
    • 사용할 수 있는 알고리즘으로는 sha256, sha512 등이 있습니다. ( 참고 )
    • 다른 sha 방식에 비해, sha512가 더 길지만 안전하므로 sha512 사용을 추천한다고 합니다.
  • update() 메서드의 인자로는 salt를 적용할 것이므로 평문 비밀번호에 salt를 더한 값을 넘겨줍니다. ( 참고 )
  • digest() 메서드의 인자로는 인코딩 방식을 넘겨줍니다. ( 참고 )
    • base64, hex 등의 방식이 있습니다.


다시 회원가입을 한 후에, 비밀번호가 어떤 모습으로 바뀌었을지 한 번 확인해보세요.




같은 알고리즘 및 인코딩 방식을 사용하면, 같은 입력 값에 대해서 같은 결과를 얻을 수 있습니다.

그렇기 때문에 로그인을 할 때 DB에 저장된 비밀번호를 복호화 할 필요가 없는 것입니다.

단, salt는 랜덤 값이기 때문에 암호화를 했을 때 사용했던 salt를 알고 있어야 합니다.

그렇기 때문에 users 테이블의 필드로 salt를 추가한 것입니다.

( 이 부분에 대해서는 다음 글에서 로그인에서 다시 언급하겠습니다. )





6. pbkd2 암호화 방식

hash이외에도 공식 문서에는 여러 암호화 알고리즘을 소개하고 있습니다.


위의 방법 중 pbkd2를 이용한 방법만 간단하게 살펴보도록 하고 마무리 짓도록 하겠습니다. ( 참고 )


/routes/users.js

router.post("/sign_up", function(req,res,next){
let body = req.body;

crypto.randomBytes(64, function(err, buf) {
crypto.pbkdf2(body.password, buf.toString('base64'), 100000, 64, 'sha512', async function(err, key){
result = await models.user.create({
name: body.userName,
email: body.userEmail,
password: key,
salt: buf
})

res.redirect("/user/sign_up");
});
});
})

  • crypto.randomBytes()를 호출함으로써 64byte의 salt를 생성합니다.
  • crypto.pbkdf2() 메서드
    • 첫 번째 인자는 사용자의 비밀번호,
    • 두 번째 인자는 salt,
    • 세 번째 인자는 반복 횟수,
      • 반복 횟수는 많으면 많을 수록 보안이 좋아집니다. ( 레인보우 테이블을 만들기 어려워집니다. )
    • 네 번째 인자는 비밀번호 길이,
    • 다섯 번째 인자는 인코딩 방식을 의미합니다.

또한 이 방식은 비동기로 동작합니다.





이상으로 회원가입 할 때 비밀번호를 암호화 하는 방법에 대해 알아보았습니다.


[ 참고 ]

https://www.zerocho.com/category/NodeJS/post/593a487c2ed1da0018cff95d


댓글 펼치기 👇
  1. 2020.01.20 14:47

    비밀댓글입니다

    • Favicon of https://victorydntmd.tistory.com victolee 우르르응 2020.01.20 18:26 신고

      결론부터 말씀드리면 불가능합니다.
      OAuth는 인가 프로토콜로서, 예를 들면 구글로부터 토큰을 발급받아 인증이 되면 유저의 구글 계정을 이용하여 본인의 사이트에 로그인을 하는 방식입니다.

      이 글은 의미가 없을거구요.
      말씀 주신대로 passport.js 모듈을 사용하면 쉽게 구현할 수 있습니다.
      직접 구현 하시려면 OAuth client를 구축해야 합니다.

    • 2020.01.20 19:16

      비밀댓글입니다

  2. 홍길동 2020.03.13 16:51

    정말 감사합니다 깔끔하고 참조도 보기 편하고 초보 개발자로서 많은 도움이 됩니다~

  3. ljh 2020.04.01 10:45

    node공부하면서 천천히 보고있는데 너무 좋아요 감사합니다!