본문 바로가기
컴퓨터/웹 : JS

[Node.js] 해시의 이해, 해시로 비밀번호 생성

by 도도새 도 2022. 11. 28.

해시의 이해

 

이번에는 해쉬 함수를 사용하여 비밀번호를 만드는 방법을 정리하고자 한다. 많은 웹은 로그인 기능을 제공한다. 그러나 우리는 <<절대로 사용자의 비밀번호를 데이터 베이스에 그대로 저장해서는 안 된다.>> 만약 그렇게 한다면 우리의 데이터 베이스를 열어보는 것만으로 사용자의 아이디와 비밀번호가 노출되기 때문이다.

 

또한 많은 유저는 여러 사이트에서 같은 비밀번호를 사용하는 경향이 있기에 더욱 문제가 심각해진다. 이런 상황에서 어떻게 데이터베이스에 사용자의 비밀번호를 저장하고 로그인 기능을 구현할 수 있을까?

해답은 해시 함수(hash function)의 사용이다.

 

해시함수(혹은 해시 알고리즘)은 임의의 길이의 데이터(여기서는 비밀번호)를 고정된 길이의 데이터로 매핑하는 함수이다. 이렇게 비밀번호를 다른 데이터로 매핑시키므로서 해당 매핑된 데이터를 원래 비밀번호로 복구할 수 없다.(알고리즘이 그런 식으로 만들어져 있다.)

 

해시 예시(cha256)와 salting

 

해시 함수는 같은 입력에 대하여 늘 같은 결과를 보장한다. 그러나 해시함수에 입력되는 값 하나만이라도 달라진다면 결과는 완전히 달라진다.

 

입력 : abcde

결과 : 36bbe50ed96841d10443bcb670d6554f0a34b761be67ec9c4a8ad2c0c44ca42c

 

입력 : abcdf

결과 : 791d36372ca8ab7619eeb71038f5f45b083577a20962aedbb9dfa05f32113b45

ef로 바뀌었을 뿐인데 결과값이 완전히 달라진 것을 확인할 수 있다.

 

Salting

 앞서 사용자는 같은 비밀번호를 계속 사용한다고 말한 바 있다. 그렇다면, 같은 해시 함수를 사용한다면 결국 저장되는 데이터는 계속해서 같을 것이다.

 

 이 문제를 해결하고자 하는 것이 salting이다. salting이란 해시함수에 값을 넣을 때, 랜덤한 데이터를 더해서 해시값을 바꾸는 것이다. 그렇다면 같은 비밀번호를 입력하더라도 salt에 의해 결과 값이 완전히 달라지게 될 것이다.

 

Node.js + bcrypt

 

Node js를 이용해 bcypt를 이용한다.

터미널을 통해 아래의 명령어를 입력한다. 

$ npm init

$ npm i bcrypt

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const bcrypt = require('bcrypt');
 
const hashPassWord = async(pw)=>{
    const salt = await bcrypt.genSalt();
    const hash = await bcrypt.hash(pw, salt);
    console.log(salt);
    console.log(hash);
}
 
const login = async(pw, hashPassWord) => {
    const result = await bcrypt.compare(pw, hashPassWord);
    if(result){
        console.log("Log In!!!");
    }
    else{
        console.log("NONONO");
    }
}
 
hashPassWord("imPassword");
login("imPassword""$2b$10$aVuA6qBHteNtsB9rx5LGoexugsrJtiTni5Nzc3c4uA/sNYhRhaDsS ");
cs

4 : genSalt(rounds, minor, cb)//salt를 생성한다.

5 : hash(data, salt, cb)//데이터와 솔트로 해시된 데이터를 생성한다.

7 : 생성된 해시 데이터를 콘솔로 확인 후, 21번째 줄에 login함수로 넘겨주었다.(즉, 한번 실행 후 21번 코드 넣고 재실행)

11 : compare(data, encrypted, cb)//입력 데이터와 해싱된 데이터를 비교한다. true false 반환

 

로그인 구현

 

Node.js에서 mongo db를 이용해 간단하게 로그인을 구현한다.

npm i express mongoose express-session bcrypt

 

user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mongoose = require('mongoose');
 
const userSchema = new mongoose.Schema({
    username:{
        type: String,
        required: [true'Username cannot ba blank']
    },
    password: {//해시된 비밀번호가 저장됨
        type: String,
        required: [true'Password cannot ba blank']
    }
});
 
module.exports = mongoose.model('User', userSchema);
cs
유저이름과 비밀번호를 담는 몽구스 스키마를 만든 후 export한다. 즉, 데이터베이스에 이름과 비밀번호를 담을 것이다. 

 

app.js

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const User = require('./models/user');
const bcrypt = require('bcrypt');
const session = require('express-session');
 
mongoose.connect('mongodb://localhost:27017/auth')
.then(()=>{
    console.log('database connect');
})
.catch(err=>{
    console.log(err);
})
 
app.use(express());
app.use(session({
    secret: 'notagoodsecret',
    resave: true,
    saveUninitialized: true}));
 
app.set('view engine''ejs');
app.set('views''views');
 
 
 
app.use(express.urlencoded({extended:true}));//body 데이터 해석하기 위함
 
const requireLogin = (req, res, next) =>{
    if(!req.session.user_id){
        return res.redirect('/login');
    }
    next();
}
 
 
app.get('/', (req, res)=>{
    res.send('home')
})
 
app.get('/register', (req, res)=>{
    res.render('register');
})
 
app.post('/register', async(req, res)=>{
    const {username, password} = req.body;
    const hash = await bcrypt.hash(password, 12);//비밀번호로 해시
    const user = new User({
        username,//유저네임은 그대로
        password: hash//비밀번호는 해시해서
    });
    await user.save();//데이터베이스에 저장
    res.redirect('/login');
})
app.get('/login', (req, res)=>{
    res.render('login');
})
app.post('/login', async(req, res)=>{
    const {username, password} = req.body;
    const user = await User.findOne({username});//데이터베이스에서 username과 일치하는 데이터 찾아 저장
    let validPassword;
    if(user == null) validPassword = false;//아이디가 존재하지 않음
    else validPassword = await bcrypt.compare(password, user.password);//비밀번호 일치 불일치 여부 판단
    //입력받은 password와 데이터베이스 내부 password비교
    if(validPassword){
        req.session.user_id = user._id;//세션에 user_id할당 후 user컬렉션의 _id 저장
        res.redirect('/secret');
    }
    else{
        res.redirect('/login');
    }
})
 
app.post('/logout', async(req, res)=>{
    //req.session.user_id = null;//세션의 user_id프로퍼티 값을 null로
    req.session.destroy();//세션을 삭제
    res.redirect('login');
})
 
 
 
app.get('/secret', requireLogin, (req, res)=>{
    res.render('secret');
})
 
 
app.listen(3000, ()=>{
    console.log("3000 is connected");
})
cs

17 : 로그인시 로그인 상태인 것을 나타내기 위해 session을 이용한다.

46 : post를 받으면 넘겨받은 username과 password을 저장한다.

45 : 비밀번호는 해시함수를 통해 해시된 데이터로 변경한다.(비밀번호를 절대 데이터베이스에 그대로 저장하지 않는다!)

52 : 데이터베이스에 컬렉션을 만든다.(아이디 생성)

58 : 받아넘겨온 정보로 데이터베이스에 아이디가 있는지 확인한다. 

66 : 세션에 user_id을 할당하여 user._id를 저장한다. 데이터베이스의 각 값은 _id가 자동으로 생성된다.

29 : 미들웨어 함수, 만약 session의 user_id에 값이 없다면, 로그인 화면으로 되돌린다.

82 : 미들웨어 함수를 이용한다. 로그인시에만 보일 수 있는 화면

76 : 세션을 삭제하여 로그아웃 처리를 한다. (user_id에 null을 넣어 로그아웃 처리하는 방법도 가능)

 

 

데이터베이스에 컬렉션(아이디) 생성 확인

 

깃허브 : https://github.com/fkthfvk112/WEB_Practice/tree/main/hashPractice

 

댓글