본문 바로가기

공부/Node.js

[Node.js] express로 웹 서버 만들기(virtual 함수와 암호화)

오늘은 보안관련해서 공부를 해보았다. 코드가 많이 바뀌어서 그렇게 스무스하게 하지는 못했지만 일단 적어가면서 나도 이해를 해야겠다.

 

개념

 

일단 virtual 함수란 가상의 함수를 만들어서 단방향 암호화로 암호화 당하는 속성을 따로 만들어준다고 생각하면 될 것 같다.

 

 

책의 암호화 방식을 흉내내어 한 번 만들어 보았다, UserSchema에 가상 속성인 Password를 사용해서 저장할 때는 Hashed_Password에 암호화하여 넣어준다. 같은 이름으로 암호화 해서 안 넣는 이유를 알고 싶긴하지만 서로를 구분하고 싶어서... 가 아닐까?(학교 강의에서 교수님이 코딩에서 존재 이유의 대부분에 가독성이 들어간다고 하긴하던데...)

 

아무튼 이런 구조를 이해하고 코드를 수정했다.

 

코드

 

function createUserSchema(){
    UserSchema = mongoose.Schema({
        id:{type : String, required : true, unique : true},
        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 : false}, 'default' : Date.now},
        updated_at:{type:Date, index : {unique : false}, 'default' : Date.now}
    });
 ...

 

이건 createUserSchema로 초기에 스키마를 설정하는 것을 따로 함수로 만들었다. 여기서 설정하는 함수는 MongoDB 내부에 설정하는 함수이고 password라는 가상 속성를 만들기 위해서는 따로 만들어 줘야한다.

 

...
    UserSchema
        .virtual('password')
        .set(function(password){
            this._password = password;
            this.salt = this.makeSalt();
            this.hashed_password = this.encryptPassword(password); //password를 암호화 하여 hashed_password로 들어감
            console.log('virtual passowrd 호출됨 : ' + this.hashed_password);
        })
        .get(function(){
        return this._password;
    });
...     

 

위에서 이어지는 코드로 password라는 가상 속성을 만들어 준다. set과 get의 역할을 따로 줘서 get일 경우 기존  password를 반환하고 set은 들어온 password와 encryptPassword를 통해서 hashed_password에 암호화된 값을 넣어준다.

 

 ...
    UserSchema.static('findById', function(id, callback){
         return this.find({id : id}, callback);
     });
        
    UserSchema.static('findAll', function(callback){
        return this.find({}, callback);
    });
    
    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');
        }
    });
    
    UserSchema.method('makeSalt', function(){
        return Math.round((new Date().valueOf() * Math.random())) + '';
    });
    
    UserSchema.method('authenticate', function(plainText, inSalt, hashed_password){
        if(inSalt) {
            console.log('authenticate 호출됨 : %s -> %s : %s', plainText, this.encryptPassword(plainText, inSalt), hashed_password);
            return this.encryptPassword(plainText, inSalt) === hashed_password;
        } else {
            console.log('autenticate 호출됨 : %s -> %s : %s', plainText, this.encryptPassword(plainText), this.hashed_password);
            return this.encryptPassword(plainText) === this.hashed_password;
        }
    });
    
    //유효성 검사로 필수 속성이 들어가 있는지 아닌지를 확인하는 코드
    UserSchema.path('id').validate(function(id){
        return id.length;
    }, 'id 칼럼의 값이 없습니다.');
    
    UserSchema.path('name').validate(function(name){
        return name.length;
    }, 'name 칼럼의 값이 없습니다.');
    
    console.log('UserSchema 정의함');
}

 

위의 코드에 이어서 createUserSchema에 들어가는 함수들이다. UserSchema가 앞에 붙어 있는 것을 보면 알 수 있겠지만 UserSchema에 각종 method들을 설정하고 전역함수도 설정하고 유효성 검사도 하고 UserSchema를 선언하는 함수라고 보면 될 것 같다.

 

여기서 encryptPassword, makeSalt, authenticate 이렇게 3가지 메소드가 암호화와 관련이 있다.

 

makeSalt : Math.random()을 이용해서 랜덤 값을 하나 만들어내는 함수

encryptPassword : 랜덤 값을 이용하여 단방향 암호화를 진행하는 함수

authenticate : 단방향 암호화된 함수와 들어온 함수를 비교하는 함수

 

마지막의 두 함수는 스키마에 id와 name이 존재하는지를 validate를 통해서 검증하는 코드이다. 실제로 id는 없으면 안되지만 name은 없을 경우 default 값이 들어가기 때문에 밑에 코드는 없어도 될 것 같다.

 

이렇게 UserSchema의 구조를 바꾸었으니 addUser와 authUser의 코드를 손봐줄 필요가 있다. 실제로 코드를 들여다보니 addUser는 바꿀 필요가 없고 authUser만 바꾸면 될 것 같다.

 

var authUser = function(database, id, password, callback){
    console.log('authUser 호출됨' + id + ', ' + password);
    
    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);
        }
    });
};

 

코드에서 바꿔줘야할 부분은 바로 아까 암호를 비교하는 부분은 따로 수정을 해줘야한다. var authenticated 줄을 보면 스키마의 함수인 authenticate를 사용해서 현재 들어온 값인 password와 salt를 이용해서 hashed_password와 비교하는 것을 볼 수 있다.

 

이렇게 암호화를 통해서 비교를 하는 부분이 들어가줘야한다.

 

결과

 

결과는 크게 볼것은 없고 사용자 추가를 통해서 hashed_password에 잘 들어가는가 확인을 해보겠다.

 

 

아이디는 password, 비밀번호는 123456, 이름은 암호화 이렇게 3가지 정보를 보내보니

 

 

로그에 호출된 password가 123456이 아닌 것을 확인 할 수 있다. 그리고 로그인을 해보면

 

 

로그인은 password에 123456으로 한다.

 

 

이렇게 정상적으로 로그인을 하게 된 것을 볼 수 있다.

 

 

ROBO 3T를 통해서 확인을 해보니 아이디 이름은 우리가 아는 그 값으로 들어갔지만 hashed_password는 password에서 암호화 된 값으로 들어 간 것을 확인 할 수 있다.

 

이래서 보안이 어렵다고 하는 것 같다. 그렇게 큰 일을 한게 아닌데도 상당히 코드가 길고 이해하는데도 좀 오래 걸렸다. 아무튼 여기까지 문제 없이 잘 완료하였다. 코드를 보는 눈이 조금씩 늘어나는 것 같다.