현재 작업중인 미니 팀 프로젝트에서 사용자 회원가입, 로그인 시 비밀번호가 평문으로 저장되고 있다. 당연히 평문으로 저장하는 것은 그리 좋은 아이디어는 아니기에, Node.js 에서 패스워드를 암호화 할 수 있는 강력한 라이브러리인 Bcrypt를 적용해보았다.
hash 라는 단어 자체가 고기, 감자따위를 잘게 다져서 섞는다는 의미이다. 다져진 감자는 다시 감자로 돌아갈 수 없듯이, 해싱을 거친 암호 원본은 다시 원본 형태로 돌아갈 수 없게된다. 즉, 단방향이다.
다져진 감자는 원재료 상태로 돌아갈 수 없다.
Bcrypt에서는 salt 라는 단계를 거쳐, 음식에 소금을 살짝 뿌려 간을 하듯 아주 작은 임의의 랜덤한 텍스트를 추가하여 원본 암호를 변형한다.
우선 사용자로부터 회원가입 시 비밀번호를 받는다.
// userRoute.js - signup api
router.post("/signup", async (req, res) => {
try {
const { loginId, password, nickname } = await postUserSchema.validateAsync(req.body)
...
const user = new Users({ userId: loginId, password, nickname })
await user.save()
...
} catch(err) {
}
})
일련의 validation 과정을 거치고, user.save()
를 통해 데이터베이스에 저장한다. 이 때, Mongoose 내에 pre
메서드를 사용하여 저장하기 전에 해싱 과정을 거친다.
// models - user.js
const bcrypt = require("bcrypt")
const saltRounds = 10;
...
userSchema.pre("save", function (next) {
const user = this; // userSchema
// user document의 'password' 가 수정되었을 경우. isModified가 document를 새로 추가하는 경우에도 동작함.
if(user.isModified('password')) {
// genSalt : salt 생성
bcrypt.genSalt(saltRounds, function(err, salt) {
if (err)
return next(err);
bcrypt.hash(user.password, salt, function (err, hashedPassword) {
if(err)
return next(err);
user.password = hashedPassword; // 평문 user.password를 hashedPassword 로 입력
next(); // save 진행
})
})
} else {
// password 값이 변경되지 않을 경우
next();
}
})
위와 같은 과정을 거쳐, 데이터베이스에 저장 시 해싱된 암호 값이 저장된다.
로그인 시에는, 사용자가 입력한 암호와 해싱된 암호 값을 비교할 수 있는 bcrypt.compare(password, hashed_password)
를 사용한다.
router.post("/login", async (req, res) => {
try {
const { loginId, password } = await postLoginSchema.validateAsync(req.body)
const user = await Users.findOne({ userId: loginId }).exec()
...
// 로그인 시 사용자로부터 입력받은 패스워드와 bcrypt를 거쳐 저장된 패스워드 비교
const isSamePassword = await bcrypt.compare(password, user.password)
if(!isSamePassword){
res.send({
ok: false,
result: "ID 또는 패스워드를 확인해주세요.",
})
return
}
...
} catch (err) {
...
}
})
사용자가 입력한 암호를 별도로 변환하지 않고 데이터베이스에 저장된 해시된 비밀번호와 비교할 수 있다.