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