유저가 클라이언트(회원가입 폼 등)에서 입력한 ID, 비밀번호, 이름 등의 정보는 데이터베이스에 저장된다.
데이터베이스에 저장되는 정보 중 특히 비밀번호와 같은 정보는 더욱 더 민감하게 다뤄져야하는데
서버 관리자와 정보를 관리하는 데이터베이스 관리자를 포함하여
유저 본인이 아닌 다른 그 누구도 알아서는 안되며,
설사 유출됐다고 하더라도 비밀번호를 해독하지 못하는 형태로 변형해줘야한다.
예를 들어 유저가 다음과 같은 정보로 회원 가입을 했다고 치자.
ID: user1
PW: 12345
이때 유저가 비밀번호로 입력한 12345
라는 그 자체의 문자열을 Plain Text
라고 한다.
비밀번호의 값을 데이터베이스에 Plain Text
인 채로 저장한다면
비밀번호가 서버 관리자와 정보를 관리하는 데이터베이스 관리자에게 유출되는 것은 물론이며,
해킹을 당했을 시 당연하게도 비밀번호가 아주 손쉽게 도용될 것이다.
이러한 사태를 막기 위해 비밀번호 해싱(Password hashing)이라는 것을 진행하는 것이다.
해싱(Hashing)
이라는 말은 무언가를 작은 조각으로 잘게 다지고 썬다는 뜻을 가진 Hash
로부터 왔다.
('해쉬'브라운 할 때 그 해쉬)
해싱(Hashing)
이란, 12345
와 같은 Plain Text
를 특정 알고리즘(Hash Function)을 통해 잘게 다져서
fd12fsf@$%4s
와 같이 인간이 해독하지 못하는 문자열(Hashed Text)로 변형해주는 것이다.
해싱(Hashing)
에 대해 짚고 넘어가야할 해싱(Hashing)
의 특징에 대해 알아보자.
아래 그림 해석:
해싱이란 이런 것을 말하는겁니다. 원래대로 다시 되돌리는 것이 불가능합니다.
해쉬 브라운을 감자로 다시 되돌릴 순 없잖아요.
해싱(Hashing)
의 가장 중요한 특징인 단방향이라는 것을 이해할 때 단번에 도움이 됐던 그림이다.
해싱(Hashing)
은 단방향
의 특징을 가지고 있기 때문에
(해싱(Hashing)이란?의 일러스트 참조. 화살표가 한방향 뿐임)
Plain Text
를 Hashed Text
로 변형시키는 것은 가능하지만
Hashed Text
를 Plain Text
로 되돌리는 것은 절대 불가능하다.
https://www.convertstring.com/ko/Hash/SHA256
위의 사이트는 웹에서 간단하게 Plain Text
를 Hashed Text
로 해싱(Hashing)
해주는 사이트이다.
입력값(Plain Text
)로 1234
를 입력하니 03AC674216F3E15C761EE1A5E255F067953623C8B388B4459E13F978D7C846F4
라는 값으로 해싱(Hashing)
되었다.
그 후 다시 한번 해싱(Hashing)
을 돌리기 위해 「해시 생성」 버튼을 여러번 클릭해도,
Hashed Text
는 변하지 않는다.
Hashing
은 동일한 입력값에 대해 동일한 출력값을 가진다는 것이 바로 이런 뜻이다.
해커들은 이 특징을 악용하여 Hashed Text
으로부터 Plain Text
를 유추한 자료인
레인보우 테이블(Rainbow table)
을 이용하여 공격을 한다.
유저들이 많이 사용하는 비밀번호와 그의 Hashed Text
를 테이블로 만들어놓고,
해킹해온 Hashed Text
의 Plain Text
를 역으로 계산하여 찾아내는 것이다.
개발자들은 이를 방어하기 위해 Salt란?🧂라는 것을 사용한다.
https://www.convertstring.com/ko/Hash/SHA256
위 사이트에서 Hello World
를 Hashing
하면
A591A6D40BF420404A011733CFB7B190D62C65BF0BCDA32B57B277D9AD9F146E
라는 결과가 출력되는데,
Hello world
를 Hashing
하면
64EC88CA00B268E5BA1A35678A1B5316D212F4F366B2477232534A8AECA37F3C
라는 결과가 출력된다.
대문자와 소문자와 같이 입력값이 아주 일부분의 사소한 차이, 한끝 차이임에도 불구하고
전혀 다른 새로운 Hashed Text
를 출력한다.
이 특징을 활용하여 보안을 더 강화하기 위해 Salt란?🧂라는 것을 사용한다.
해싱(Hashing)의 특징 중 2️⃣와 3️⃣에서
해싱(Hashing)의 취약점 때문에 Salt
를 활용한다고 했는데, Salt
란 무엇일까?
Salt
는 아주 작은 임의의 랜덤한 텍스트를 말하는데 음식에 소금을 살짝 뿌려 간을 하듯이,
Salt
란 실제 비밀번호(Plain Text)에 Salt
라는 랜덤한 값을 추가해서
Hashed Text
를 구하는 방법이다.
Plain Text에 랜덤한 텍스트인 Salt를 추가해서 Hashing
을 하면
Hashing
의 3️⃣의 특성으로 인해, 전혀 다른 Hashed Text
가 탄생되기 때문에
해커가 Hashed Text
를 이용하여 Plain Text
역으로 유추하기 어렵게 만든다.
Plain Text + Salt ⏩ Hashing ⏩ Hashed Text
이번에 사용해볼 Bcrypt
란, Plain Text
를 Hashed Text
로 변형해주는 Hash Function
의 한 종류이다.
이번 글에서는 Bcrypt
라는 Hash Function
라이브러리에 Salt
를 활용해서
비밀번호를 해싱(Hashing)
하고 데이터베이스에 저장해보는 시간을 가져 보고자 한다.
Bcrypt는 단방향 Hash Function
라이브러리 중 하나로, 비밀번호를 해싱(Hashing)
해준다.
npm install bcrypt --save
이번 예제에서는 Node.js(Express)
에서 MongoDB
(데이터베이스)에 Mongoose
를 이용하여
회원 정보(email, 비밀번호)를 저장해보고자 한다.
이때 회원 정보(email, 비밀번호)가 MongoDB
에 저장되기 앞서서
먼저 비밀번호를 해싱(Hashing)
해주는 처리를 해야 한다.
(맨처음에 설명했듯이 비밀번호가 Plain Text
인 채로 저장되면 안되니까.)
// controllers/registerController.js
import User from "../models/User.js";
export const postRegister = (req, res) => {
const user = new User(req.body);
user.save((err, userInfo) => {
if (err) return res.json({ success: false, err })
return res.status(200).json({ success: true })
})
}
위의 코드는 클라이언트로부터 받은 회원 정보를
MongoDB
(데이터베이스)에 저장하는 기능을 담당하는 Register Controller
코드이다.
이 코드의 save
라는 MongoDB
메소드가 발동되어
회원 정보가 MongoDB
에 저장되기 전에 먼저 비밀번호를 해싱(Hashing)
해줘야 한다.**
그래서 Mongoose
의 pre
라는 메소드를 활용한다.
Mongoose
의 pre
메소드는 Register Controller
의 save
메소드가 실행되기 전에 실행된다.
save
되기 전에 비밀번호를 Hashing
하기 위해,
pre
메소드 내부에 Hash Function인 Bcrypt
를 작성한다.
// models/User.js
import mongoose from "mongoose";
import bcrypt from "bcrypt"; // Bcrypt를 불러옴
const saltRounds = 10; // salt 돌리는 횟수
// Schema 정의
const userSchema = new mongoose.Schema({
name: {
type: String,
maxlength: 30
},
email: {
type: String,
trim: true,
unique: 1
},
password: {
type: String,
minlength: 5
},
...
})
// Mongoose의 pre메소드는 `Register Controller`의 save메소드가 실행되기 전에 실행된다.
// save되기 전에 Hashing을 하기 위해 pre메소드 내부에 Hash Function 작성
userSchema.pre("save", function (next) {
const user = this; // userSchema를 가르킴
if (user.isModified('password')) {
// password가 변경될 때만 Hashing 실행
// genSalt: salt 생성
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err);
bcrypt.hash(user.password, salt, function (err, hashedPassword) {
// hash의 첫번째 인자: 비밀번호의 Plain Text
if (err) return next(err);
user.password = hashedPassword; // 에러없이 성공하면 비밀번호의 Plain Text를 hashedPassword로 교체해줌
next(); // Hashing이 끝나면 save로 넘어감
})
})
} else {
// password가 변경되지 않을 때
next(); // 바로 save로 넘어감
}
})
const User = mongoose.model("User", userSchema);
export default User;
먼저 터미널에서 서버를 실행시켜준다.
(각자의 스크립트에 맞춰서 실행)
npm run dev:server
그다음 응답 결과를 확인하기 위해 Insomnia라는 툴에서 테스트를 진행했다.
비밀번호의 Plain Text
는 임의로 1234567
설정해서 Request를 보냈다.
응답 결과는 에러 없이 성공했다는 뜻으로 무사히 success
에 true가 들어왔다.
비밀번호의 Plain Text
인 1234567
가 잘 Hashing
되었는지 확인하기 위해
실제로 MongoDB
에 저장된 값을 살펴보자.
비밀번호가 $2b$10$hOshyAvL1SEW4hMWFGQyw..z8B9kP8F1EOB5JwRJbFcqyoQIa87Ri
라는 값으로
잘 Hashing
된 것을 알 수 있다!
https://stackoverflow.com/questions/14588032/mongoose-password-hashing
미흡하고 틀린 정보가 있다면 지적 부탁드립니다!🙇♀️