빠르게 훝어보는 node.js
#14 - Passport를 이용한 사용자 인증 구축
조대협 (http://bcho.tistory.com)
Passport는 node.js용 범용 인증 모듈이다. 기본적인 HTTP Basic Auth에서 부터 HTTP digest authentication, OAuth,OpenID 등 다양한 인증 프로토콜을 지원하며, Facebook이나 Twitter,Google등과의 연계된 SSO 인증을 포함하여 약 140가지의 인증 모듈을 포함한다. 그러면Passport를 이용하여, 어떻게 사용자 인증을 구현할 수 있는지 살펴보도록 하자.
이러한 다양한 인증에 대해서 미리 구현해 놓은 것을 passport에서는 Strategy라고 한다. Facebook 인증을 위해서는 facebook strategy를 Twitter 인증을 위해서는 Twitter Strategy를 사용한다.
Local Strategy
먼저 id,password를 받아서 자체 인증 처리를 하는 가장 간단한 LocalStrategy에 대해서 살펴보면서 개념을 이해해보도록 하자. Express 프로젝트를 생성한후
먼저 웹 인증을 하기 위해서는 HTML Form으로 부터 사용자 id와 passwd를 받기 위한 페이지를 생성하자.
HTML파일은 express에서 static 페이지를 저장하는 /public 디렉토리 아래에 /public/login.html로 아래와 같이 정의 하였다.
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">
<div>
<label>UserId:</label>
<input type="text" name="userid"/><br/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Submit"/>
</div>
</form>
</body>
</html>
이 폼에서, 사용자 id는 “userid”라는 필드로, 사용자 비밀번호는 “password”라는 이름의 필드로 입력 받도록 하였다.
Passport의 LocalStrategy 는 디폴트로, username이라는 필드와 password라는 필드로 각각 로그인 id와 비밀번호를 받도록 되어 있는데, 이 예제에서는 username 대신 앞의 폼에서 정의한 userid라는 필드로 사용자 id를 입력 받도록 변경할것이다.
app.js에서 Passport와 local strategy를 사용하기 위해서 모듈을 불러 들이자.
var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;
다음으로, passport의 LocalStrategy를 정의하고, Local Strategy에 의해서 인증시에 호출되는 인증 메서드를 정의한다..
passport.use(new LocalStrategy({
usernameField : 'userid',
passwordField : 'password',
passReqToCallback : true
}
,function(req,userid, password, done) {
if(userid=='hello' && password=='world'){
var user = { 'userid':'hello',
'email':'hello@world.com'};
return done(null,user);
}else{
return done(null,false);
}
}
));
LocalStrategy 객체를 생성한 후에, 첫번째로 세팅값을 설정한다.
usernameField와 passwordField라는 인자는,LocalStrategy가 HTML 폼으로 어떤 필드를 각각 사용자 id와 pasword로 읽어드릴지를 정의하는 옵션이다. 별도로 정의하지 않았을 경우 “username”과 “password”라는 필드를 사용하지만, 여기서는 “userid” 라는 폼 필드로 부터 값을 읽어드리도록 변경하였다.
passReqToCallback이라는 옵션은 인증을 수행하는 인증 함수로 HTTP request를 그대로 전달할지 여부를 결정한다.
다음으로는 인증시에 호출되는 인증 함수를 정의한다.인증 함수로는 4개의 인자가 전달된다. HTTP request를 전달하는 req와, 사용자 id,passwd를 전달하는 인자가 각각 들어가고 마지막으로 callback 함수인 done을 전달한다.
이 함수에서, 실제로 사용자 인증을 수행하게 된다.
이 예제는 아주 간단한 예제이기 때문에, 사용자 id가 “hello”이고, 비밀번호가 “world”일 경우 인증이 성공한것으로 처리하도록 하였다. 실제 프로그램에서는 사용자 데이타베이스에서 사용자 id와 비밀번호를 조회해서 비교하도록 하면 된다.
인증이 성공하였을 경우 done callback함수에, null과 함께, user라는 인자를 넘기게 된다.
user라는 인자는 객체로 사용자에 대한 로그인 정보를 저장하는 객체로 사용자 이름이나, ID등의 정보를 저장할 수 있다. 로그인 후에는 HTTP Session에 저장된다. 이 예제에서는 간단하게, 사용자 id와 사용자 email만 저장하도로 하였다.
만약에 인증이 실패하였을 경우에는 done callback함수에 done(null,false)로 리턴하게 되면 인증 실패 메세지를 내게 된다.
Serialize와 Deserialize
로그인이 성공하면, serializeUser 메서드를 이용하여 사용자 정보를 Session에 저장할 수 있다.
passport.serializeUser(function(user, done) {
console.log('serialize');
done(null, user);
});
serializeUser 메서드에서는 function(user,done)을 이용해서 session에 저장할 정보를 done(null,user)과 같이 두번째 인자로 넘기면 된다. 이때 user로 넘어오는 정보는 앞의 LocalStrategy 객체의 인증함수에서 done(null,user)에 의해 리턴된 값이 넘어온다.
이 예제에서는 이 user 객체 전체를 사용자 session에 저장하였다.
다음으로, node.js의 모든 페이지에 접근할때, 로그인이 되어 있을 경우 모든 사용자 페이지를 접근할 경우 deserilizeUser가 발생한다. deserializeUser에서는 session에 저장된 값을 이용해서, 사용자 Profile을 찾은 후, HTTP Request의 리턴한다.
// 인증 후, 페이지 접근시 마다 사용자 정보를 Session에서 읽어옴.
passport.deserializeUser(function(user, done) {
//findById(id, function (err, user) {
console.log('deserialize');
done(null, user);
//});
});
deserializeUser의 callback함수의 첫번째 인자로 넘어오는 내용”user”는 세션에 저장된 사용자 정보이다. 이 예제에서는 session에 해당 사용자의 정보를 저장하였기 때문에 별도의 변경없이 done(null,user)를 이용해서 그대로 session에서 읽은 내용을 리턴한다.
이렇겍 리턴된 내용은 HTTP Request 에 “req.user” 값으로 다른 페이지에 전달된다.
설명이 좀 복잡한데, 배경을 설명하면 다음과 같다. Session에 사용자 정보를 저장하고자할 경우, 사용자 정보가 크다면, 메모리가 많이 소모되기 때문에, serializeUser시에, 사용자 id와 같은 키 정보만 저장하도록 하고, 페이지가 접근될때 마다 deserilizeUser가 수행되면,세션에 저장된 사용자 id를 이용하여 데이타베이스에서 사용자 정보를 추가로 select해서 HTTP request에 붙여서 리턴하는 형태를 사용한다.
실제로도 PassPort의 공식 메뉴얼을 보면 다음과 같이 가이드를 하고 있다.
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
serialize시에 session에서는 user.id인 사용자 id값만 저장하고, deserialize시에는 session에 저장된 id를 이용해서, DB에 매번 사용자 정보를 select하는 모습을 볼 수 있다.
그러나 이렇게 하게 되면, 실제 Session에 저장되는 데이타가 적어서 메모리는 어느 정도 절약할 수 있지만 페이지 접근시마다 매번 DB select가 발생하기 때문에 성능에 많은 저하가 오기 때문에 권장하고 싶지 않다. 아주 큰 데이타를 넣지 않는 이상 10~20개정도의 필드는 HTTP Session에 저장해도 큰 문제가 없기 때문에, 앞에서 필자가 소개한 방식대로 사용자 로그인 데이타를 모두 serialize시에 session에 넣는 것을 권장한다. 만약에 데이타가 너무 많아서 메모리 사용량이 우려될 경우에는 redis와 같은 외부 메모리 DB를 이용해서 session 정보를 저장하도록 하는 방법을 사용하자.
다음으로, Express 객체를 생성한 후에, express에서 passport를 initialize하고, passport에서 session을 사용하도록 설정한다.
app.use(passport.initialize());
app.use(passport.session());
Route 모듈과 연동
이제 LocalStrategy에 대한 인증 모듈과 인증후 사용자 정보를 세션에 넣고 빼는 serilizeUser와 deserializeUser 메서드를 구현했으니 이제, login 창에서 들어오는 userid와 passwd를 가지고 인증 모듈을 실행하도록 연결을 해보자.
Passport는 connect 미들웨어의 형태로 동작한다. 특정 URL로 들어오는 log in 요청에 대해서 connect 미들웨어 형태로, passport 인증 모듈을 수행하도록 할 수 있다, Express의 route 미들웨어 등과 통합이 가능하다. passport.authenticate라는 메서드를 이용하는데,
app.get(‘/URL’,passport.authenticate(‘{strategy}’),function(req,res){
:
}
이 코드는 ‘/URL’로 들어오는 요청에 대해서 사용자 인증을 처리하도록 passport.authentication 을 중간에 미들웨어 형태로 끼워 놓은 것이다. 인증에 성공하게 되면, 뒤에 정의된 callback 함수 인 function(req,res)로 요청이 연결되고, 만약에 인증이 실패하면 401 Unauthorized HTTP response를 리턴한다. Authenticate의 첫번째 인자로 정의된 ‘{strategy}’는 passport의 인증 Strategy를 정의한다.
위의 코드에서 처럼 passport 미들웨어에서 인증이 완료된 call back function을 실행하는 방법 말고, 인증 성공/실패에 따라서 다른 페이지로 redirect할 수 있도록 설정할 수 있다.
app.post(‘/URL,passport.authenticate(‘{strategy}’,{successRedirect:’/success’
failureRedirect:’/failure’});
위의 코드는 인증이 성공했을때에는 /success URL로 리다이렉트하고, 실패했을 경우에는 /failure라는 URL로 redirect하도록 한다
그러면 두번째 소개한 redirect 방식으로, passport.authenticate 메서드를 연결해보자
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login_fail', failureFlash: true }),
function(req, res) {
res.redirect('/login_success');
});
login.html에서 POST action으로 들어온 인증처리를 /login에서 하도록 하고 passport.authenticate를 ‘local’ strategy로 호출한다.
그리고 로그인이 성공했을 경우에는 ‘/login_sucess’로 redirect하고, 실패했을 경우에는 ‘/login_fail’로 redirect하도록 하였다.
페이지별로 로그인이 되었는지를 확인하고, 로그인이 되어 있을 경우 HTTP request에서 사용자 정보를 가지고 오는 코드는 다음과 같다.
app.get('/login_success', ensureAuthenticated, function(req, res){
res.send(req.user);
// res.render('users', { user: req.user });
});
request.user를 이용하면, deserializeUser에 의해서 저장된 사용자 정보를 꺼내볼 수 있다
Connect 미들웨어의 특성을 이용하여, 매 호출시마다 ensureAuthenticated라는 메서드를 호출하게 해서, 로그인이 되어 있는지를 확인하도록 할 수 있는데,
function ensureAuthenticated(req, res, next) {
// 로그인이 되어 있으면, 다음 파이프라인으로 진행
if (req.isAuthenticated()) { return next(); }
// 로그인이 안되어 있으면, login 페이지로 진행
res.redirect('/login.html');
}
와 같이 로그인이 되어 있으면, req.isAuthenticated()가 true로 리턴된다.
지금까지 설명한 내용을 정리해보자
passport에서 인증은 express router에서 passort.authenticate 메서드를 정의함으로써 인증 메서드를 호출할 수 있다. 이때 passport에서 사용한 인증 Strategy를 정의한다.
authenticate에서 정의한 인증 Strategy에 대한 개게를 passport.use를 이용해서 정의하고, 이 안에, 인증 함수를 구현한다. 이 인증함수에서는 HTML form 필드를 통해서 받은 id와 password가 전달되는데, DB등을 연동하여, 들어온 id와 passwd를 비교한후에, 성공하였을 경우 done(null,user) 정보로 리턴을 한다.
이렇게 인증 성공으로 리턴이 되면 사용자에 대한 정보를 세션에 저장할 수 있도록 passport.serailizeUser로 앞에서 done(null,user)를 통해서 전달한 user 정보가 전달된다.
그러면 passport.serializeUser에서는 전달된 user 객체중에서 세션에 저장할 정보만을 done(null,user)로 HTTP session에 저장한다.
일단 인증이 되어 로그인이된 사용자는 매 request마다 passport.desrializeUser 메서드를 호출하게 되는데, 앞에서 passport.serializeUser에서 저장한 사용자 id 키를 이용해서, 사용자 정보를 DB등에서 조회하거나 하여, done(null,user)로 리턴하면, HTTP request에 함께 리턴이 된다.
해당 사용자가 로그인이되어 있는지를 확인하려면 req.isAuthenticated를 이용하여 로그인 되었는지 여부를 확인할 수 있고, 페이지별로 로그인이 되어 있는지에 대해서 접근제어를 하려면 함수를 하나 정의한 후에, Express의 router에 connect 미들웨어로 전처리로 호출하게 해서 인증 여부를 확인하도록 하면 된다.
지금까지 간단하게 LocalStrategy를 통해서 Passport에 대해서 알아보았다.다음에는 Passport-facebook Strategy를 통해서 OAuth2와 facebook 인증에 대해서 알아보도록 하겠다.
sample code : https://github.com/bwcho75/node.js_study/tree/master/Passport-Localstrategy
'클라우드 컴퓨팅 & NoSQL > Vert.x & Node.js' 카테고리의 다른 글
빠르게 훝어보는 node.js 온라인 강좌 (0) | 2014.07.18 |
---|---|
빠르게 훝어 보는 node.js - #15 - Passport를 이용한 OAuth 2.0 API 인증 (Facebook 1/2) (3) | 2014.06.30 |
빠르게 훝어 보는 node.js - #13 Socket.IO 클러스터링 (1) | 2014.05.06 |
빠르게 훝어 보는 node.js - #12 Socket.IO 4/4 - 채팅방 기능 추가하기 (4) | 2014.04.24 |
빠르게 훝어 보는 node.js - #11 Socket.IO 3/4 (1:1 귓속말 구현) (3) | 2014.04.24 |