일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Exercism
- 자바스크립트
- 장고 개발 순서
- til
- 북마크앱
- mongodb
- 장고 프로젝트
- 독립영화플랫폼
- 알고리즘
- 프로젝트
- 예술영화추천
- 타사인증
- 장고
- JavaScript
- 파이썬 웹프로그래밍 장고
- Django Blog
- passport.js
- Blog
- python
- ART_Cinema
- MyPick31
- 장고 프로젝트 순서
- 개발
- MYSQL
- Node.js
- Django
- join()
- 북마크만들기
- Algorithm
- Bookmark
- Today
- Total
Juni_Dev_log
(node.js) [Part.6] 데이터 베이스 사용하기 - 비밀번호 암호화하여 저장하기 본문
로그인에 사용되는 비밀번호를 암호화하려면 어떻게 해야할까?
몽구스를 사용하면 스키마나 모델 객체에 함수를 추가하고 필요할 때 그 함수를 실행할 수 있으니 비밀번호를 암호화하여 저장하는 기능을 구현하면 된다.
이 기능을 어떻게 구현하는지 잘 살펴보면 몽구스와 몽고디비에 대해서 더 깊게 이해할 수 있을 것이다.
virtual 함수 사용하기
실무에서는 사용자 정보를 데이터베이스에 저장할 때 비밀번호를 사람들이 볼 수 없도록 암호화하여 저장하는 경우가 많다. 이때 비밀번호는 단방향으로 암호화하여 원본 비밀번호 문자열을 알 수 없도록 만든다.
▶ 단방향 암호화라는 것이 어떤 의미일까?
단방향 암호화란 말 그대로 한 방향으로만 암호화가 가능한 것이다.
이와 반대되는 것이 양방향 암호화이다. 예를들어, 단방향 암호화는 hello 라는 글자를 암호화했다면 암호화된 데이터는 다시 원본인 hello 로 복구할 수 없다.
원본 글자로 복구하는 과정을 복호화(Description)라고 하는데 복호화할 수 있는 방법이 없으므로 한쪽 방향으로만 암호화가 가능하다는 의미로 단방향 암호화라고 부른다.
이에 반해, 양방향 암호화는 hello 라는 글자를 암호화했다면 암호화된 데이터를 다시 원본이 hello 로 복호화할 수 있다.
사용자 인증 과정에서 사용자가 입력한 비밀번호를 암호화한 후 데이터베이스에 저장되어 있는 암호화된 비밀번호와 비교하는 과정을 거친다.
사용자를 추가할 때는 비밀번호를 암호화한 후 데이터베이스에 저장한다.
그다음 사용자 인증을 진행할 때는 클라이언트가 보내 온 비밀번호를 암호화한 후 데이터베이스에 저장된 암호화된 비밀번호를 비교한다.
비밀번호 암호화 작업은 사용자 정보를 저장할 때 이루어져야한다.
mongoose 에서 제공하는 virtual() 함수를 사용하면 이 과정을 더 쉽게 처리할 수 있다.
virtual() 함수는 문서 객체에 실제로 저장되는 속성이 아니라 가상의 속성을 지정할 수 있다.
예를 들어, 비밀번호를 저장하기 위해 문서 객체에 만들어지는 속성이름이 hashed_password 라면, 실제로 저장되는 속성은 아니지만 password 속성을 만든 후 이 속성에 값을 넣어 저장할 때 hashed_password 속성으로 저장되도록 만들 수 있다.
물론 password 속성 값은 hashed_password 속성 값과 같을 필요는 없으며, 필요한 과정을 거친 후에 저장할 수 있다.
또한, password 속성 값을 조회할 때도 원하는 과정을 거친 후에 조회할 수 있다.
다음 그림을 보면 사용자를 추가하거나 인증할 때 password 속성에 접근하지만 실제 데이터베이스에는 password 속성은 없고 hashed_password 속성만 존재한다.
password 속성에 값을 넣은 후 문서 객체를 저장할 때 set() 메소드로 지정한 함수가 필요한 작업을 수행하며,
문서 객체를 조회할 때는 get() 메소드로 지정한 함수가 실행한다.
스키마 객체의 virtual() 함수 사용법 알아보기
스키마 객체의 virtual() 함수를 사용하는 방법을 알아보기 위해서 프로젝트 폴더 안에 virtual_test1.js 파일을 만든 후 데이터베이스에 문서 객체를 저장하거나 조회하는 코드를 입력한다.
이 파일에는 express 모듈을 사용하지 않고 단순히 노드로 테스트하는 코드를 입력한다.
데이터베이스에는 users4 컬렉션으로 문서 데이터를 저장하거나 조회하고 id와 name 속성을 포함하도록 만들것이다.
virtual() 함수를 사용하여 info 속성을 추가한 후 띄어쓰기 문자로 이어진 사용자 아이디와 비밀번호 값을 info 속성에 저장한다.
info 속성에 저장하면 그 속성 값을 띄어쓰기 문자로 분리한 후 id 와 name 속성에 각각 값을 나누어 저장한다.
그리고 info 속성 값을 조회하여 가져올 때는 id 와 name 속성 값을 불러서 가져온다.
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
|
...
// ===== 모듈 불러들일기 =====
var mongodb = require('mongodb');
var mongoose = require('mongoose');
// ===== 데이터베이스 연결 =====
var database;
var UserSchema;
var UserModel;
// 데이터베이스에 연결하고 응답 객체의 속성으로 db 객체 추가
function connectDB(){
// 데이터베이스 연결 정보
var databaseUrl = 'mongodb://localhost:27017/local';
// 데이터 베이스 연결
mongoose.connect(databaseUrl);
database = mongoose.connection;
database.on('error', console.error.bind(console, 'mongoose connection error'));
database.on('open', function(){
console.log('데이터베이스에 연결되었습니다. : ' + databaseUrl);
// user 스키마 및 모델 객체 생성
createUserSchema();
// test 진행함
doTest();
});
database.on('disconnected', connectDB);
}
...
|
cs |
먼저 require() 메소드로 mongodb 와 mongoose 모듈을 불러들인다.
데이터베이스 객체, 스키마 객체, 모델 객체를 할당할 변수로 선언한 후, connectDB() 함수를 정의한다.
이 connectDB() 함수코드의 가장 마지막에 호출한다.
connectDB() 함수 안에서 데이터베이스에 연결하는 코드는 app4.js 파일에서 데이터베이스를 연결하기 위해 사용한 코드와 비슷하다.
데이터베이스가 정상적으로 열린 후에 테스트를 위해 정의한 함수를 호출할 수 있도록 open 이벤트를 처리하는 코드를 입력한다. 데이터베이스가 열리면, 스키마를 정의하는 createUserSchema() 함수를 호출한 후, 테스트를 위해 정의한 doTest() 함수를 호출한다.
createUserSchema() 함수의 코드는 다음과 같다.
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
|
...
// user 스키마 및 모델 객체 생성
function createUserSchema(){
// 스키마 정의
// password를 hashed_password 로 변경, default 속성 모두 추가, salt 속성 추가
UserSchema = mongoose.Schema({
id : {type:String, required: true, unique : true},
name : {type:String, index: 'hashed', 'default':''},
age : {type:Number, 'default':-1},
created_at : {type:Date, index:{unique: false}, 'default': Date.now},
updated_at : {type:Date, index:{unique: false}, 'default': Date.now}
});
// info를 virtual 메소드로 정의
UserSchema
.virtual('info')
.set(function(info){
var splitted = info.split(' ');
this.id = splitted[0];
this.name = splitted[1];
console.log('virtual info 설정함 : %s, %s', this.id, this.name);
})
.get(function(){return this.id + ' ' + this.name});
console.log('UserSchema 정의함.');
//UserModel 모델 정의
UserModel = mongoose.model('users4', UserSchema);
console.log('UserModel 정의함.');
}
...
|
cs |
스키마 객체에는 id , name 을 포함해 다섯 개의 속성이 정의된다.
그다음에는 스키마 객체의 virtual() 함수를 사용해 info 속성을 지정한다. 이 속성은 지금부터 스키마에 정의된 다른 속성처럼 사용할 수 있다.
virtual() 함수로 속성을 지정하면 set() 메소드와 get() 메소드를 호출할 수 있다.
set() 메소드에서는 전달된 파라미터를 띄어쓰기 기호로 분리하기 위해서 split() 메소드를 사용한다.
예를 들어, 'test01 소녀시대' 라는 글자를 info 속성 값으로 지정하면 두 개의 아이템을 가지는 배열 객체가 만들어진다.
그 첫번째 아이템은 id 속성에 , 두 번째 아이템은 name 속성에 저장한다.
get() 메소드에서는 id 와 name 속성 값을 붙여서 반환한다. 스키마를 정의했다면 이 스키마로 모델 객체를 만든다.
모델 객체는 데이터베이스의 users4 컬렉션을 사용하도록 지정한다.
데이터베이스가 연결된 후에 호출되는 doTest() 함수의 코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
...
function doTest(){
//UserModel 인스턴스 생성
// id, name 속성은 할당하지 않고 info 속성만 할당함
var user = new UserModel({"info" : 'test01 소녀시대'});
// save() 로 저장
user.save(function(err){
if(err){throw err;}
console.log('사용자 데이터 추가');
findAll();
});
console.log('info 속성에 값 할당함.');
console.log('id : %s, name : %s',user.id , user.name);
}
...
|
cs |
데이터베이스에 문서 데이터를 저장하기 위해 new 연산자로 모델 인스턴스 객체를 만든다.
모델 인스턴스 객체를 만들 때 전달하는 속성에는 info 속성만 지정한다. 모델 인스턴스 객체를 저장할 때, info 속성의 값을 띄어쓰기 기호로 분리하여 저장하므로 id 나 name 속성은 지정할 필요가 없다.
데이터가 저장되면 findAll()
1
2
3
4
5
6
7
8
9
10
11
|
...
function findAll(){
UserModel.find({}, function(err, results){
if(err) {throw err;}
if(results){
console.log('조회된 user 문서 객체 #0 -> id : %s, name : %s', results[0]._doc.id, result[0]._doc.name);
}
});
}
...
|
cs |
모든 데이터를 조회한 후에는 id, name 속성 값을 확인하도록 작성되어있다.
코드의 마지막 부분에서는 connectDB() 메소드를 호출하여 데이터베이스 연결, 문서 스키마 정의, 문서 데이터 저장, 문서 데이터 조회를 순서대로 실행한다.
virtual_test1.js 파일을 실행하면 다음과 같이 users4 테이블에 문서 객체를 저장한 후 조회하면서 콘솔 창에 메세지가 표시된다.
id 와 name 속성 값이 잘 저장된 것을 볼 수 있다.
비밀번호 암호화하여 저장하는 코드 적용하기
virtual () 함수로 가상 속성을 지정하는 방법에 대해서 이해했다면, app4.js 파일을 복사하여 app5.js 를 만들고 crypto 모듈을 설치한다.
% npm install crypto --save
그리고 비밀번호를 암호화하여 저장하는 코드를 입력한다.
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
|
...
// crypto 모듈 불러들이기
var crypto = require('crypto');
...
// 데이터베이스에 연결
function connectDB(){
// 데이터베이스 연결 정보
var databaseUrl = 'mongodb://localhost:27017/local';
// 데이터베이스에 연결
console.log('데이터베이스 연결을 시도합니다.');
mongoose.Promise = global.Promise;
mongoose.connect(databaseUrl);
database = mongoose.connection;
// 에러가 발생했을 때 이벤트
database.on('error', console.error.bind(console, 'mongoose connection error'));
// 데이터베이스를 열었을 때 이벤트
database.on('open', function(){
console.log('데이터베이스에 연결되었습니다. : ' + databaseUrl);
//user 스키마 및 모델 객체 생성
createUserSchema();
});
// 연결 끊어졌을 때 5초 후 재연결
database.on('disconnected', function(){
console.log('연결이 끊어졌습니다. 5초 후 다시 연결합니다.');
setInterval(connectDB, 5000);
});
}
...
// user 스키마 및 모델 객체 생성
function createUserSchema(){
// 스키마 정의
// passowrd 를 hashed_password 로 변경, default 속성 모두 추가, salt 속성 추가
UserSchema = mongoose.Schema({
id : {type: String, required: true, unique : true, 'default': ' '},
hashed_password : {type: String, required: true, 'default': ' '},
salt : {type: String, required: true},
name : {type: String, index: 'hashed', 'default': ' '},
age : {type: Number, 'default': -1},
created_at : {type: Date, index:{unique:true}, 'default': Date.now},
updated_at : {type: Date, index:{unique:true}, 'default': Date.now}
});
...
}
|
cs |
노드는 암호화를 위해 crypto 모듈을 제공한다.
이 모듈을 사용해서 암호화할 것이므로 require() 메소드로 crypto 모듈을 불러들인다. 스키마를 정의할 때는 기존에 만들어 둔 id, name, age, created_at, updated_at 속성 외에 hashed_password 와 salt 속성을 추가한다.
hashed_password 속성에는 비밀번호를 암호화하여 저장하고 salt 속성에는 암호화에 사용하는 salt 값을 저장한다.
암호화 과정에서는 일종의 키값으로 salt 값을 사용하는데 이 값이 계속 변경되도록 만들 것이다.
따라서 암호화된 비밀번호와 함께 데이터베이스에 저장한다. 기존에 사용한 password 속성은 더 이상 사용하지 않도록 설정하고 virtual() 함수를 사용해 가상 속성으로 추가한다.
다음은 virtual() 함수를 사용해 password 속성을 스키마에 추가하는 코드이다.
1
2
3
4
5
6
7
8
9
10
11
|
// password 를 virtual 메소드로 정의 : MongoDB 에 저장되지 않은 편리한 속성임.
// 특정 속성을 지정하고 set, get 메소드를 정의함.
UserSchema
.virtual('password')
.set(function(password){
this._password = password;
this.salt = this.makeSalt();
this.hashed_password = this.encryptPassword(password);
console.log('virtual password 호출됨 : ' + this.hashed_password);
})
.get(function(){return this._password});
|
cs |
virtual() 메소드를 호출할 때는 가상의 속성의 이름으로 password를 전달한다.
set() 함수는 전달된 문자열을 암호화된 비밀번호로 만든 후, hashed_password 속성으로 저장한다.
암호화를 진행하기 위해 makeSalt() 와 encryptPassword() 메소드를 호출한다. 이 두 메서드는 다음과 같이 정의한다.
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
|
// 스키마에 모델 인스턴스에서 사용할 수 있는 메소드 추가
// 비밀번호 암호화 메소드
UserSchema.method('encryptPassword', function(plainText, inSalt){
if(inSalt){
return crypto.createHmac('sha1', inSalt).update(plainText).digest('hex');
}else{
return crypto.createHmac('sha1', this.salt).update(plainText).digest('hex');
}
});
// salt 값 만들기 메소드
UserSchema.method('makeSalt', function(){
return Math.round((new Date().valueOf()*Math.random())) + '';
});
// 인증 메소드 - 입력된 비밀번호와 비교 (true/false 리턴)
UserSchema.method('anthenticate', function(plainText, inSalt, hashed_password){
if(inSalt){
console.log('anthenticate 호출됨 : %s -> %s : %s',plainText, this.encryptPassword(plainText, inSalt), hashed_password);
return this.encryptPassword(plainText, inSalt) === hashed_password;
}else{
console.log('authenticate 호출됨 : %s -> %s : %s',plainText, this.encryptPassword(plainText), this.hashed_password);
return this.encryptPassword(plainText) === this.hashed_password;
}
});
|
cs |
makeSalt() 메소드와 encryptPassword() 메소드는 스키마 객체의 method() 메소드를 사용해 모델 인스턴스 객체에서 호출할 수 있는 메소드로 추가한다.
makeSalt() 메소드는 Math.random() 메소드를 호출화면서 랜덤 값을 하나 만들어 낸다.
encryptPassword() 메소드는 비밀번호와 salt 값을 파라미터로 전달받은 후 crypto 모듈로 암호화한다.
authenticate 메소드가 하나 더 추가되었는데, 이 메소드는 파라미터로 전달된 비밀번호와 암호화된 비밀번호가 같은지 비교한다. 데이터베이스에 저장할 문서 데이터 중에서 필수 속성인데 값이 없는 경우를 체크하기 위해 validate() 함수를 사용할 수 있다.
다음 코드는 id 나 name 속성 값이 없는 경우를 확인하기 위해 추가된 것이다.
1
2
3
4
5
6
7
8
|
// 필수 속성에 대한 유효성 확인 (길이 값 체크)
UserSchema.path('id').validate(function(id){
return id.length;
}, 'id 칼럼의 값이 없습니다.');
UserSchema.path('name').validate(function(name){
return name.length;
}, 'name 칼럼의 값이 없습니다.');
|
cs |
스키마 객체의 path() 메소드를 호출한 후 validate() 메소드를 호출하면 유효한 값인지 확인할 수 있다.
id 속성 값을 확인하여 입력된 값이 유효한지 알려준다.
이제 사용자를 추가하는 addUser() 함수의 코드를 수정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 사용자를 추가하는 함수
var addUser = function(database, id, password, name, callback){
console.log('addUser 호출됨 : ' + id + ', ' + password);
//UserModel 의 인스턴스 객체 생성
var user = new UserModel({"id":id, "password":password, "name":name});
// save()로 저장
user.save(function(err){
if(err){
callback(err,null);
return;
}
console.log('사용자 데이터 추가함.');
callback(null,user);
});
}
|
cs |
먼저 new 연산자로 모델 인스턴스 객체를 만든다.
모델 인스턴스 객체를 만들 때는 password 속성에 클라이언트로부터 전달받은 비밀번호 값을 넣는다.
save() 메소드를 호출하여 저장하면 비밀번호가 암호화된 후 hashed_password 속성에 저장한다.
사용자를 인증하는 authUser() 함수의 코드도 스키마에 추가한 authenticate 함수를 호출하는 방식으로 수정한다.
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
|
// 사용자를 인증하는 함수 : 아이디로 먼저 찾고 비밀번호를 그 다음 비교
var authUser = function(database, id, password, callback){
console.log('authUser 호출됨 : ' + id + ', ' + password);
// 1. 아이디를 사용해서 검색
UserModel.findById(id, function(err,results){
if(err){
callback(err,null);
return;
}
console.log('아이디 [%s]로 사용자 검색 결과',id);
console.dir(results);
if(results.length > 0){
console.log('아이디와 일치하는 사용자 찾음.');
var user = new UserModel({id : id});
var authenticated = user.authenticate(password, results[0]._doc.salt, results[0]._doc.hashed_password);
if(authenticated){
console.log('비밀번호 일치함');
callback(null,results);
}else{
console.log('비밀번호 일치하지 않음.');
callback(null,null);
}
}else{
console.log('아이디와 일치하는 사용자를 찾지 못함.');
callback(null,null);
}
});
}
|
cs |
모델 인스턴스 객체를 만들 때는 id 속성에만 값을 넣어준다. 모델 인스턴스 객체의 authenticate 메소드를 호출할 때 클라이언트로부터 전달받은 암호와 함께 아이디를 이용해 찾아낸 문서 객체의 salt 값과 hashed_password 값을 파라미터로 전달한다.
이제 코드를 모두 수정했으니 app5.js 파일을 실행한 후 웹 브라우저를 열고 사용자 등록 사이트에 접속한다.
hashed_passowrd 속성 값이 암호화되어 있다는 것을 확인할 수 있다.
salt 속성에도 값이 들어있는데 이 값은 문서 객체가 저장될 때마다 변경된다. 다시 한 번 웹 브라우저를 열고 로그인 사이트에 접속한다.
http://localhost:3000/public/login.html
아이디와 비밀번호를 입력하면, 성공 메세지를 볼 수 있으며, 틀린 비밀번호를 넣은면 로그인 실패 메세지가 표시된다.
virtual 메소드를 이용해 스키마에 가상 속성을 추가하는 것은 조금 복잡해 보일 수 있지만 코드를 한 번 만들어 두면 단순히 속성에 값을 설정하거나 조회하는 것만으로도 암호화 기능을 수행할 수 있어서 매우 유용하다.
물론 이 메소드는 암호화 이외의 다른 기능을 만들 때도 사용한다.
지금까지 몽고디비에서 사용자 정보를 저장하고 조회하는 방법에 대해서 알아보았다.
단순히 사용자를 추가한 후 사용자 리스트를 조회하는 기능에서부터 사용자 비밀번호를 암호화하는 기능까지 여러번 반복해서 실습해보자.
데이터베이스를 다루는 방법에 익숙해지는 것이 최선이다.
'Theorem (정리) > node.js' 카테고리의 다른 글
(node.js) [Part.7] 익스프레스 프로젝트를 모듈화하기 - 모듈화 방법 자세히 살펴보기 (0) | 2021.01.21 |
---|---|
(node.js) [Part.6] 데이터 베이스 사용하기 - MySQL 데이터베이스 사용하기 (0) | 2020.12.27 |
(node.js) [Part.6] 데이터 베이스 사용하기 - 인덱스와 메소드 사용하기 (0) | 2020.12.20 |
(node.js) [Part.6] 데이터 베이스 사용하기 - 몽구스로 데이터베이스 다루기 (0) | 2020.12.18 |
(node.js) [Part.6] 데이터 베이스 사용하기 - 익스프레스에서 몽고디비 사용하기 (0) | 2020.12.18 |