ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 14장 passport 모듈을 이용한 사용자 인증 (1)
    MeanStack (deprecated) 2016. 6. 23. 22:04



    패스포트는 노드 모듈이고 , 패스포트라는 이름에서 알 수 있듯이 이는 사용자 인증을 위한 외부 모듈이다. 


    패스포트는 전략이라는 메커니즘을 사용해 개발자에게 다양한 인증 방식을 제공한다. 


    백문이 불여일견 , 일단 설치하고 직접 해보자 . 




    간단하게 설치를 하였다 . 


    이제 passport 를 사용하기에 앞서 passport 의 설정파일을 만들자.  config 폴더로 가서 passport.js 파일을 만들자.


    일단 파일만 만들고 지금은 잠시 놔두자. 이 파일을 불러오기 위해 StartApp.js 파일을 수정하자.



    process.env.NODE_ENV = process.env.NODE_ENV || 'development';
    
    var express = require('./config/express_config'),
        mongoose = require('./config/mongoose'),
        passport = require('./config/passport');  // 추가 
    
    
    
    var db = mongoose();
    var app = express();
    var passport = passport();    // 추가 
    
    
    app.listen(3000);
    module.exports = app;
    
    console.log('Server running at localhost');
    
    
    



    방금 만든 파일을 불러오는 코드를 삽입하였다. 


    이제 express 에도 passport 미들웨어를 등록하자.  config 폴더에 express_config.js 파일을 수정하자. 


    var express = require('express'),
        morgan = require('morgan'),
        compress = require('compression'),
        bodyParser = require('body-parser'),
        methodOverride = require('method-override'),
        config = require('./config'),
        session = require('express-session'),
        passport = require('passport')    //추가 
        ;
    
    module.exports = function() {
        var app = express();
    
        if(process.env.NODE_ENV === 'development') {
            app.use(morgan('dev'));
        } else if (process.env.NODE_ENV === 'production') {
            app.use(compress());
        }
    
        app.use(bodyParser.urlencoded({
            extended : true
        }));
        app.use(bodyParser.json());
        app.use(methodOverride());
    
        app.use(session({
            saveUninitialized : true,
            resave : true,
            secret : config.sessionSecret
        }));
    
        app.set('views','./app/views');
        app.set('view engine', 'ejs');
    
        app.use(passport.initialize());    // 추가 
        app.use(passport.session());       // 추가 
    
        require('../app/routes/index.server.routes.js')(app);
        require('../app/routes/users.server.routes.js')(app);
        app.use(express.static('./static'));
    
        return app;
    }
    
    
    



    코드를 살펴보면 passport 모듈을 require 로 올리고 , express 에 passport.initialize() 라는 미들웨어와 


    passport.session() 미들웨어를 추가 하였다. initialize() 는 패스포트 모듈을 초기화 시키며 구축하고 , 


    session() 은 사용자의 세션을 추적하기 위한 모듈이다. 


    패스포트를 설치 했으니 이젠 사용해보자 !!! 일단 패스포트를 사용하려면 일단 최소한 인증 전략을 하나 이상은 


    등록해야 사용할 수 있다. 먼저 맛보기로 단순히 사용자이름과 패스워드 인증을 제공하는 지역 전략부터 만들어 


    보겠다.  먼저 지역전략을 사용하기 위해 passport-local 이라는 모듈을 설치해야 한다. 


    설치 하였으면 이제 지역 전략을 구성하기 위해 config 폴더에 strategies 라는 새로운 폴더를 만들자. 


    전략마다 독자적인 파일을 구성해야 하기 때문에 strategieslocal.js 파일을 만들고 코드를 입력 하자.



    var passport = require('passport'),
        LocalStrategy = require('passport-local').Strategy,
        User = require('mongoose').model('User');
    
    module.exports = function() {
        passport.use(new LocalStrategy(function(username, password , done) {
            User.findOne({
                userid : username
            }, function(err, user){
                if(err) {
                    return done(err);
                }
    
                if(!user) {
                    return done(null, false, {
                        message : 'Unknown user'
                    });
                }
                if(!user.authenticate(password)) {
                    return done(null, false, {
                        message : 'Invalid password'
                    });
                }
                return done(null, user);
            });
        }));
    };
    
    


    코드를 살펴보자. 먼저 passport 모듈과 passport-local 의 Strategy 객체 , 그리고 인증을 사용할 User 몽구스 


    모델을 require 로 올리는 작업부터 시작한다.  그리고 나서, passport.use() 메소드로 LocalStrategy 객체를 


    등록한다.  LocalStrategy 메소드는 인수로 콜백함수를 받는다. 


    이 콜백함수는 username , password , done 을 인수로 받으며 콜백함수 내부를 살펴보자.


    내부에서는 User 몽구스 모델을 사용해 사용자가 입력한 username 과 일치하는 username 을 User 모델에서 


    찾을 것 이다.  찾은 뒤 username 이 일치 하지 않은면 'Unknown user' 라는 메시지로 응답 할 것 이고 


    만약 username 이 같다면 password 를 비교하고 이 역시 같은지 틀린지 비교하여 틀리다면 'invalid password' 


    메시지를 retuen 할 것 이다. 


    여기서는 userId 라는 필드를 로그인 아이디로 사용할 것 이기 때문에 username 인수는 userid : username 으로


    대입하여 users 모델에서 userid 로 검색하도록 하자. 


    이젠 위에서 작성한 passport.js 파일을 채울 때가 되었다.  config 폴더로 가서 passport.js 파일을 수정하자. 



    var passport = require('passport'),
        mongoose = require('mongoose');
    
    module.exports = function() {
        var User = mongoose.model('User');
    
        passport.serializeUser(function(user, done){
           done(null, user.id);
        });
    
        passport.deserializeUser(function(id, done){
            User.findOne({
                _id : id
            }, '-password -salt', function(err, user){
                done(err, user);
            });
        });
        require('./strategies/local.js')();
    };
    
    


    위 코드에서 serializeUser() 와 deserializeUser() 메소드는 passport 모듈이 사용자 직렬화를 다루는 방식을 


    정의하기 위해 사용된다. 사용자를 인증할 때, 패스포트는 세션에 _id 속성을 저장할 것 이다. 


    나중에 user 객체가 필요할 때, 패스포트는 _id 속성을 사용해 데이터베이스에서 user 객체를 가져올 것 이다.


    몽구스가 password 와 salt 속성을 사져오지 않게 보증하기 위해 필드 옵션을 사용하는 방법에 주목하자.


    또한 위 코드는 지역 전략 구성 파일을 포함한다.  


    이런 식으로 StartApp.js 파일은 패스포트 구성파일을 읽어 들이며 , 계속해서 전략구성 파일을 읽어 들일 것이다.


    다음으로 패스포트의 인증을 지원하기 위해 User 모델을 변경할 필요가 있다. 


    app/models 폴더로 가서 user.server.model.js 파일을 수정하자. 




    var mongoose = require('mongoose'),
        crypto = require('crypto'),   // 추가
        Schema = mongoose.Schema;
    
    var UserSchema = new Schema({
        username : String ,
        userid :  {
            type : String ,
            unique : true ,                           // 추가
            required : 'Username is required'  ,       // 추가
            trim : true
        },
        password : {                                     // ------password 변경
            type : String ,                              //
            validate : [                                 //
             function(password) {                        //
                return password && password.length > 6;  //
             }, 'Password should be longer'              //
            ]                                            //
        },                                               // ------password 변경 끝
    
        salt : {                                         // ------salt 추가
            type : String                                //
        },                                               // ------salt 추가 끝
    
        provider : {                                     // ------provider 추가
            type : String ,                              //
            required : 'Provider is required'            //
        },                                               // ------provider 추가 끝
    
        providerId : String ,                            // ------providerId 추가
    
        providerData : {} ,                              // ------providerData 추가
    
        email : {                                                           // email 변경
            type : String ,                                                 //
            match : [/.+\@.+\..+/, "pleas fill a valid e-mail address"]     //
        } ,                                                                 // email 변경 끝
        created : {
            type : Date,
            default : Date.now
        }
    });
    
    
    UserSchema.pre('save', function(next){                                      // ------------ 추가 메소드
        if(this.password) {
            this.salt = new Buffer(crypto.randomBytes(16).toString('base64'),
            'base64');
        this.password = this.hashPassword(this.password);
        }
        next();
    });
    
    UserSchema.methods.hashPassword = function(password) {
        return crypto.pbkdf2Sync(password, this.salt, 10000, 64).
        toString('base64');
    };
    
    UserSchema.methods.authenticate = function(password) {
        return this.password === this.hashPassword(password);
    };
    
    UserSchema.statics.findUniqueUserid = function(userid, suffix, callback) {
        var _this = this;
        var possibleUserid = userid + (suffix || '');
    
        _this.findOne({
            userid : possibleUserid
        }, function(err,user) {
            if(!err) {
                if(!user) {
                    callback(possibleUserid);
                }else{
                    return _this.findUniqueUserid(userid, (suffix || 0) + 1, callback);
                }
            }else{
                callback(null);
            }
        });
    };                                                                               // 추가 끝
    
    
    
    UserSchema.set('toJSON',{ getters : true , virtuals : true});
    mongoose.model('User',UserSchema);
    
    


    자 첫번째 난관이 왔다. " 이게 뭐야 뭐가 추가된거야 저건 왜 추가된거야   에이 안해  " 하고 끝날 수 있는 


    첫번째 난관이다. 하지만 이를 넘는다면 angular.js 라는 상품(?) 이 기다리고 있다. 


    조금만 참고 천천히 분석해보자.  


    가장 먼저 UserSchema 객체에 필드를 4개 추가 하였다.  암호를 해시하기 위한 salt 속성 , 사용자를 등록하기 


    위해 사용되는 전략을 지시하는 provider 속성 , 인증전략을 위한 사용자 식별자를 지시하는 providerId 속성 , 


    OAuth 공급자로부터 인출한 사용자 객체를 저장하기 위해 나중에 사용 할 providerData 속성이다. 



    다음으로, 사용자의 비밀번호를 해시하기 위해 pre-save 미들웨어를 생성했다. 


    pre 미들웨어는 해당요청이 실행되기전에 먼저 실행되는 메소드를 지정할 수 있다.  예를 들어 기존에 만들었던 


    신규 사용자 등록을 위해 post 로 데이터를 보냈을 시 데이터를 등록하기 전 pre 미들웨어가 있다면


     pre 미들웨어를 먼저 실행하고 데이터를 등록할 것 이다. 



    여기 pre 미들웨어 에서는 사용자의 password 를 평문이 아닌 암호화 시켜서 저장하기 위해 존재한다.


    먼저 자동으로 생성된 가상 난수 해시 솔트를 만들고 , 다음으로 현재 사용자의 비밀번호를 hashPassword()


    메소드를 사용해 암호화된 비밀번호로 치환한다. 


    여기서 hashPassword() 메소드는 노드의 crypto 모듈을 활용해 비밀번호를 암호화 하기 위해 사용되며 ,


    authenticate() 메소드는 문자열 인수를 받아들여 암호화하고 현재 사용자의 비밀번호와 비교한다. 


    마지막으로, 새로운 사용자가 선택 가능한 유일한 이름을 찾기 위해 쓰이는 findUniqueUserId() 정적 메소드를


    추가 하였다. 니중에 OAuth (트위터, 페이스북 연동 로그인) 인증을 다룰 때, 이 메소드를 사용할 것 이다. 




    이렇게 해서 UserSchema 의 변경도 완료 하였다. 


    하지만 아직 끝나지 않았다.  user controller 도 변경해야 하고 인증을 위한 ejs 뷰 역시 만들어야 한다. 


    라우팅 처리도 해야 하고 , 사용자에게 보여 줄 오류 메시지 역시 뷰에서 바로 출력할 수 있도록 해야 한다. 


    헉헉



    일단 이번장에서는 여기까지 하고 다음 장에는 남은 작업을 처리해보자. 



    (끝) 




                                                                  (최종 폴더 구조)



    댓글