블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

빠르게 훝어보는 node.js

#16  - Passport를 이용한 OAuth 2.0 API 인증 (Facebook 2/2)

조대협 (http://bcho.tistory.com)


node.js에서 페이스북 로그인하기

앞에서(http://bcho.tistory.com/913) 설명한 Facebook 로그인 시나리오를 기반으로 하여 node.js passport-facebook 모듈을 이용해서, 간단한 로그인 서비스를 만들어보자.

시나리오

만들고자 하는 시나리오는 다음과 같다.



/login 페이지에서 페이스북 로그인 버튼이 출력된다.



페이스북 로그인 버튼을 누르면 /auth/facebook 페이지로 이동한다.

/auth/facebook 페이지에서는 페이스북에 로그인을 하기 위한 OAuth 요청을 passport-facebook 모듈을 이용해서 만든후, 페이스북 로그인 페이지로 Oauth 요청과 함께 redirect한다.



페이스북 로그인 페이지에서 계정과 비밀번호를 입력하면 해당 앱에 대한 권한을 허용할것인지를 페이스북이 물어본다.



확인을 누르면, 로그인이 성공하고, 로그인 결과를 /auth/facebook/callback으로 redirect한다.

페이지에서는 페이스북에서 전달해온 사용자 정보를 받아서, node.js 세션에 저장한다. 후에, /login_success 페이지로 이동한다.

/login_success 페이지에서는 세션에 저장된 페이스북 사용자 정보를 JSON 문서 형태로 출력한다.



이것이 우리가 지금부터 구현하고자 하는 흐름이다.

2. passport-facebook 모듈을 이용하여 구현하기

그러면 node.js 이용해서 구현을 해보자.

개발환경에서,

Ÿ   npm –g install passport

Ÿ   npm –g install passport-facebook

명령을 이용하여 passport passport-facebook 모듈을 설치한다.



다음으로 https://developers.facebook.com 로그인해서새로운 앱을 등록한다.



Settins 메뉴로 이동하여 +Add Platform 버튼을 클릭한다여기서는 웹사이트를 Facebook 계정으로 연동할 예정이기 때문에, WebSite 선택한다.



 

WebSite 플랫폼을 추가하면 아래와 같이 WebSite 메뉴가 생성되고, Site URL 넣을 있는 페이지가 나온다여기서는 개인 노트북 개발환경에서 node.js 띄워놓고 개발할것이기 때문에로컬 사이트의 URL 적어 놓자. 예제에서는 노트북에서 3000 포트에 node.js 띄울 것이기 때문에, URL 아래와 같이 “http://localhost:3000”으로 적어놓는다.



다음으로 코드를 구현한다.

Express 애플리케이션을 생성한다. ( 예제는 3.x버전을 기준으로 하고 있다.)

다음으로 /public 디렉토리에 index.html 다음과 같이 구현한다.

<!DOCTYPE html>

<html>

<head>

    <title></title>

</head>

<body>

 <button onclick="location.href='/auth/facebook'">Facebook login</button>

</body>

</html>

 

다음으로 코드를 구현해보자. 아래는 /app.js 소스코드이다.

var express = require('express');

var routes = require('./routes');

var user = require('./routes/user');

var http = require('http');

var path = require('path');

 

var passport = require('passport')

    , FacebookStrategy = require('passport-facebook').Strategy;

 

// serialize

// 인증후 사용자 정보를 세션에 저장

passport.serializeUser(function(user, done) {

    console.log('serialize');

    done(null, user);

});

 

 

// deserialize

// 인증후, 사용자 정보를 세션에서 읽어서 request.user 저장

passport.deserializeUser(function(user, done) {

    //findById(id, function (err, user) {

    console.log('deserialize');

    done(null, user);

    //});

});

 

passport.use(new FacebookStrategy({

        clientID: ' 페이스북 개발자 사이트에서 찾아서 넣으세요 ',

        clientSecret: '페이스북 개발자 사이트에서 찾아서 넣으세요',

        callbackURL: "http://localhost:3000/auth/facebook/callback"

    },

    function(accessToken, refreshToken, profile, done) {

        console.log(profile);

        done(null,profile);

    }

));

var app = express();

 

// all environments

app.set('port', process.env.PORT || 3000);

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

app.use(express.favicon());

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(express.session({ secret: 'your secret here' }));

app.use(passport.initialize());

app.use(passport.session());

app.use(app.router);

app.use(express.static(path.join(__dirname, 'public')));

 

// development only

if ('development' == app.get('env')) {

  app.use(express.errorHandler());

}

 

app.get('/auth/facebook', passport.authenticate('facebook'));

app.get('/auth/facebook/callback',

    passport.authenticate('facebook', { successRedirect: '/login_success',

        failureRedirect: '/login_fail' }));

app.get('/login_success', ensureAuthenticated, function(req, res){

    res.send(req.user);

});

app.get('/logout', function(req, res){

    req.logout();

    res.redirect('/');

});

function ensureAuthenticated(req, res, next) {

    // 로그인이 되어 있으면, 다음 파이프라인으로 진행

    if (req.isAuthenticated()) { return next(); }

    // 로그인이 안되어 있으면, login 페이지로 진행

    res.redirect('/');

}

 

http.createServer(app).listen(app.get('port'), function(){

  console.log('Express server listening on port ' + app.get('port'));

});

 

 

Passport  passport-facebook 이용하기 위해서 다음과 같이 모듈을 import한다.

var passport = require('passport')

    , FacebookStrategy = require('passport-facebook').Strategy;

 

다음으로 이전 글의 LocalStrategy (http://bcho.tistory.com/920 참고) 에서 했던것 처럼, passport serializer deseriazlier 구현한다.

 

// serialize

// 인증후 사용자 정보를 세션에 저장

passport.serializeUser(function(user, done) {

    console.log('serialize');

    done(null, user);

});

 

 

// deserialize

// 인증후, 사용자 정보를 세션에서 읽어서 request.user 저장

passport.deserializeUser(function(user, done) {

    //findById(id, function (err, user) {

    console.log('deserialize');

    done(null, user);

    //});

});

 

Serailizer LocalStrategy 예제 부분에서도 언급했지만, 로그인이 성공하였을 , 인증 후에, 세션에 사용자 정보를 저장하는 기능을 한다. 로그인 성공후 값이 user라는 인자를 통해서 전달되는데, 값을 done(null,user) 넣으면 HTTP session내에 저장된다. 예제에서는 페이스북에서 로그인 후에 넘어온 정보를 그대로 저장했지만 정보의 양이 많을 경우 메모리를 절약하기 위해서 사용자 정도만 세션에 저장하는 것이 좋다.

다음으로 Deserialize 매번 페이지 접근시 마다, 세션에 저장된 사용자 정보를 읽어서 HTTP request 객체에 user라는 객체를 추가로 넣어서 리턴한다.

이는 앞서 LocalStrategy 부분에서도 설명했지만, 세션에 많은 데이타를 넣게 되면 메모리 소모가 많기 때문에,세션에는 사용자 ID정도를 저장하고 상세 사용자 정보는 DB 별도의 외부 캐쉬 (Redis 같은) 곳에 정보를 저장해놓고 세션에 저장된 사용자 ID, 추가 사용자 정보를 읽어서, 정보를 user 객체에 넣어서 done(null,user) 호출해주면, request.user 통해서 값을 전달할 있다.

Passport Facebook Strategy 불러와서 정의한다.

passport.use(new FacebookStrategy({

        clientID: ' 페이스북 개발자 사이트에서 찾아서 넣으세요 ',

        clientSecret: ' 페이스북 개발자 사이트에서 찾아서 넣으세요 ',

        callbackURL: "http://localhost:3000/auth/facebook/callback"

    },

    function(accessToken, refreshToken, profile, done) {

        console.log(profile);

        done(null,profile);

    }

));

FacebookStrategy 정의할때 인자로 client ID,clientSecret 등을 넣어야 하는데, 값들은 페이스북 개발자 사이트에서 등록된 애플리케이션 정보를 보면 AppID App Secret이라는 값이 있는데,



App ID 값을 client ID, 그리고App Secret 값을 client Secret 입력하면 된다.

그리고, 코드내에서는 콜백함수를 정의하는데, 콜백함수에는 크게 4가지 인자가 전달된다.

Ÿ accessToken : OAuth accessToken이다. 토큰을 이용해서 필요하다면 페이스북의 오픈 API (REST API) 호출할 있다. 만약에 개발하고자하는 웹서비스가 페이스북 API 호출할 경우 accessToken session등에 저장해놓고 사용하기 바란다.

Ÿ   RefreshToken : OAuth refreshToken으로, accessToken 만료되었을 재발급을 요청하는 용도로 사용된다.

Ÿ   profile : 로그인한 페이스북의 사용자 정보를 리턴한다. 예제에서는 값을 모두 세션에 저장해놓고 사용했지만 앞에 seriazlier에서도 설명했듯이 가능하면 id 정도만 세션에 저장해서 메모리 사용량을 절약하기를 바란다.

다음으로 passport 사용하기 위해서 passport 미들웨어를 use 연결한다.

app.use(passport.initialize());

app.use(passport.session());

미들웨어는 HTTP 요청이 들어왔을때 정의된 순서에 따라서 순차적으로 실행되기 때문에 이때 순서에 주의하도록한다.

여기까지 준비되었으면, passport-facebook 모듈을 사용하기 위한 모든 준비가 되었다. 이제 페이지를 구현해보자

app.get('/auth/facebook', passport.authenticate('facebook'));

 

/auth/facebook 페이지에 대한 구현으로, 페이지에 들어오면 passport facebook strategy 모듈이 OAuth 요청을 만들어서 페이스북 로그인 페이지로 OAuth 요청을 Query String 실어서 redirect한다.

app.get('/auth/facebook/callback',

    passport.authenticate('facebook', { successRedirect: '/login_success',

        failureRedirect: '/login_fail' }));

 

다음으로 페이스북 로그인 페이지에서 로그인에 대한 결과를 처리하는 페이지로, /auth/facebook/callback 페이지로, 로그인 성공을 하면 /login_sucess 페이지로 리다이렉트하고, 실패하면 /login_fail 페이지로 리다리렉트 한다.

app.get('/login_success', ensureAuthenticated, function(req, res){

    res.send(req.user);

});

 

다음은 로그인 성공 페이지로, 로그인이 성공하면 세션에 저장된 페이스북 사용자 정보를 화면에 출력한다. 여기에 ensureAuthenticated라는 메서드를 중간에 인자로 넘겼는데, LocalStrategy 예제와 마찬가지로

function ensureAuthenticated(req, res, next) {

    // 로그인이 되어 있으면, 다음 파이프라인으로 진행

    if (req.isAuthenticated()) { return next(); }

    // 로그인이 안되어 있으면, login 페이지로 진행

    res.redirect('/');

}

 

인증이 되었는지 여부를 체크하고, 만약에 인증이 안되어 있다면 / 페이지로 리다이렉트 한다. 이는 로그인이 안된 상태에서 직접 /login_success 페이지로 접근할 경우, 다시 로그인 페이지를 보여주기 위함이다.

app.get('/logout', function(req, res){

    req.logout();

    res.redirect('/');

});

 

마지막으로 로그아웃페이지를 구현한다. /logout으로 들어가면 현재 로그인 되어 있는 세션을 로그아웃 처리한다. 이때 req.logout() 메서드를 호출하게 되면 passport에서 로그아웃 처리가 된다.

지금까지 간단하게나마, passport-facebook 모듈을 이용한 페이스북 로그인 서비스를 구현하였다. 빠르게 설명하기 위해서 가장 간단한 필수 기능만을 구현하였다 추가적인 사용정보를 받아들여서 저장하는 부분등은 직접 구현하기 바란다. J

예제에 대한 소스코드는

 https://github.com/bwcho75/node.js_study/tree/master/passport-facebook 있다. (모듈 디렉토리 제외 되어있음. app.js index.html 참고할것)

 

페이스북의 Photo 서비스 시스템 아키텍쳐
The Photos application is one of Facebook’s most popular features. Up to date, users have uploaded over 15 billion photos which makes Facebook the biggest photo sharing website. For each uploaded photo, Facebook generates and stores four images of different sizes, which translates to a total of 60 billion images and 1.5PB of storage. The current growth rate is 220 million new photos per week, which translates to 25TB of additional storage consumed weekly. At the peak there are 550,000 images served per second. These numbers pose a significant challenge for the Facebook photo storage infrastructure.

1.5 PB의 사진 저장. 2009년 아키텍쳐 문서

http://www.facebook.com/note.php?note_id=76191543919

얻은 아이디어
- 하드웨어 디자인시, 디스크에 Write Back 캐쉬 적용

하나의 이미지 파일이 올라오면 4개의 Resize된 thumnail을 유지.
하나의 PhotoKey에 대해서, 1,2,3,4 thumnail에 대한 reference를 저장
Photo Key + Thumnail reference 를 Index형태로 저장하고 메모리에 유지하여 성능 향상

물리 저장 파일과 (HeyStack Store File)
Index 파일을 나눠서 저장하는 데이타 구조 (HeyStack Index File)-Index 파일은 물리 저장 파일을 통해서 다시 Build가 가능
물리 파일을 먼저 기록한 다음, Index를 Async 방식으로 기록하는 방식. Index 파일은 위와 같은 이유로 Less Critical
Delete Operation은 Index Flag에 deleted로 표시 해놓고, 물리 저장 파일은 Compaction 단계에서 collection
Index는 모두 Memory에 Loading하는 방식을 사용

Summary
Write를 Append 방식으로 하고, search나 read 성능 향상을 위해서 index 파일을 별도 유지하고, index는 memory에 로딩되는 방식을 사용. Update나 Delete는 New file을 append하고, Compaction 단계에서 Copy&Scavange와 유사한 방식으로, deleted로 mark된 needle(file)을 제거하고 복사하는 형태를 사용함



 

Introduction of Cassandra

카산드라는 구글의 BigTable 컬럼 기반의 데이타 모델과 FaceBook에서 만든 Dynamo의 분산 모델을 기반으로 하여 제작되어 Facebook에 의해 2008년에 아파치 오픈소스로 공개된 분산 데이타 베이스 입니다. 기존의 관계형 데이타 베이스와 다르게 SQL을 사용하지 않는 NoSQL의 제품중의 하나이며, 대용량의 데이타 트렌젝션에 대해서 고성능 처리가 가능한 시스템이다.(High-Scale). 노드를 추가함으로써 성능을 낮추지 않고 횡적으로 용량을 확장할 수 있다.

 얼마전에 트위터도 MySQL에서 Cassandra로 데이타베이스를 전환하였다고 한다..

자바로 작성되었음에도 불구하고, 데이타베이스라는 명칭에 걸맞게 여러 프로그래밍 언어를 지원합니다. Ruby,Perl,Python,Scala,Java,PHP,C# 

데이타간의 복잡한 관계 정의(Foreign Key)등이 필요없고, 대용량과 고성능 트렌젝션을 요구하는 SNS (Social Networking Service)에 많이 사용되고 있습니다. 성능이나 확장성과 안정성이 뛰어나지만 안타깝게도 Global Scale (여러 국가에 데이타 센터를 분리 배치하여 배포하고, 데이타 센타간 데이타를 동기화 하는 요구사항) 은 지원하지 않습니다. Global Scale이 필요하다면, MySQL기반의 geo replication Sharding이 아직까지는 가장 널리 쓰이는 아키텍쳐 같습니다

Data Model

카산드라의 데이타 모델은 다음과 같다.

전통적인 관계형 데이타 베이스와 다른 구조를 가지고 있다.먼저 데이타 모델에 대한 개념을 잡아보면

Column
컬럼은 컬럼 이름과, 값으로 이루어진 데이타 구조체이다.

{name: “emailAddress”, value:”cassandra@apache.org”}
{name:”age” , value:”20”}

Column Family

컬럼 패밀리는 컬럼들의 집합이다. 관계형 데이타 베이스의 테이블을 생각하면 되는데, 약간 그 개념이 다르다. 차이점은 나중에 설명하기로 하고, 컬럼 패밀리는 하나의 ROW를 식별하기 위한 Key를 갖는다. 하나의 Key에 여러개의 컬럼이 달려 있는 형태가 컬럼 패밀리이다.

하나의 Row를 예를 들어보면

Cassandra = { emailAddress:”casandra@apache.org” , age:”20”}

과 같은 형태이다. Cassandra가 해당 Row에 대한 Key가 되고, emailAddress age라는 이름의 두개의 컬럼을 가지고 있으며 각 컬럼의 값은 “casandra@apache.org” “20”이다.

여러개의 Row를 가지고 UserProfile이라는 이름의 컬럼 패밀리를 보면

UserProfile={
  Cassandra={ emailAddress:”casandra@apache.org” , age:”20”}
  TerryCho= { emailAddress:”terry.cho@apache.org” , gender:”male”}
  Cath= { emailAddress:”cath@apache.org” , age:”20”,gender:”female”,address:”Seoul”}
}

과 같이 표현할 수 있다. 여기서 주목할만한 점이 각 Row의 데이타 스키마가 다르다는 것이다. Cassandra Row emaillAddress age라는 컬럼을 가지고 있고, Terry.Cho emaillAddress gender라는 컬럼을 가지고 있다. 이 처럼 카산드라는 각 Row마다 다른 형태의 데이타 스키마를 가질 수 있는데, 이러한 특징은 “Schemeless”라고 한다.(키에 바인딩되는 데이타 구조는 같은 컬럼 패밀리라도 각 키별로 다를 수 있다.)

KeySpace

KeySpace는 논리적으로 ColumnFamily를 묶어주는 개념입니다. 단지 묶어만 줄뿐 데이타 구조나 관계에서는 별다른 영향을 주지 않습니다.

Super Column & Supper Column Family

앞에서 설명드렸던 컬럼에서 컬럼의 Value String이나 Integer와 같은 Primitive형 뿐만 아니라 컬럼 자체가 다시 들어갈 수 있습니다. 예를 들어 이런 구조입니다.

{name:”username” 
 value: firstname{name:”firstname”,value=”Terry”} 
 value: lastname{name:”lastname”,value=”Cho”} 
}

username이라는 컬럼 안에 firstname lastname이라는 두개의 컬럼이 들어가 있는 구조입니다.

마찬가지 형태로 Column Family 안에도 Column Family가 들어가는 Super 구조가 가능합니다.

UserList={ 
   Cath:{ 
       username:{firstname:”Cath”,lastname:”Yoon”}
       address:{city:”Seoul”,postcode:”1234”}
           }
    Terry:{ 
       username:{firstname:”Terry”,lastname:”Cho”}
       account:{bank:”hana”,accounted:”1234”} 
           }
 }

UserList라는 Column Family 안에, 각각 Cath Key username address라는 Column Family를 가지고 있고, Terry라는 Key username account라는 Column Family를 가지고 있습니다.  

Data Model for Java Developer

간단하게 카산드라의 데이타 구조에 대해서 살펴보았는데, 자바 개발자분이시라면 HashTable이 떠오를겁니다. 데이타 모델을 HashTable과 비교해서 설명해보면 다음과 같은 형태가 됩니다.코드로 이야기 하면 대략 다음과 같은 형태가 되겠지요


앞서 들었던 Column Family의 데이타 구조를 자바 코드로 표현하면 다음과 같은 구조가 됩니다.

UserProfile={
  Cassandra={ emailAddress:”casandra@apache.org” , age:”20”}
  TerryCho= { emailAddress:”terry.cho@apache.org” , gender:”male”}
  Cath= { emailAddress:”cath@apache.org” , age:”20”,gender:”female”,address:”Seoul”}
}

자바 코드

class Keyspace{
           HashTable keyspaces = new HashTable();          

           createColumnFamily(String name){
                     keyspaces.put(name,new HashTable);
           }

           putValue(String columnFamily,String key,Object value){
                     Hashtable cf = keyspaces.get(columnFamily);
                     cf.put(key,value);
           }
}

 

class TerryVO{ // Terry is a Key
           String emailAddress; // each column
           String gender;
           // setter & getter
}

 class CathVO{ // Cath is a Key

           String emailAddress;
           String age;
           String gender;
           // setter & getter 
}

KeySpace myspace;
myspace.createColumnFamily("UserProfile");
myspace.putValue("UserProfile","TerryCho",new TerryVO("terry.cho@apache.org","male");
myspace.putValue("UserProfile","Cath",new CathVO("cath@apache.org","20","female")

 자바 개발자분들이시라면 쉽게 이해하실 수 있을것 같고
구조를 분석하다보니 오라클의 데이타 그리드 솔루션은 Coherence와 데이타 구조가 매우 유사합니다. 요즘 이게 유행인가 보네요

Cassandra Test

개념을 이해했으면 실제 테스트를 한번 해보도록 하겠습니다.

먼저 아파치 카산드라 프로젝트(http://incubator.apache.org/cassandra/) 에서 카산드라를 다운 받습니다. 압축을 푼후에 bin/cassandra.bat를 실행시킵니다. (클러스터로 기동할 수 도 있으나 여기서는 단순하게 하나의 노드만 뛰어보도록 합니다.)

이제 카산드라 커맨드 라인 인터페이스(CLI)를 시키고(/bin/cassandra-cli.bat) 다음 카산드라 노드에 연결합니다. 포트는 디폴트로 9160 포트가 지정되어 있으며 /conf/storage-conf.xml에서 Listen Address Port를 변경할 수 있습니다.  

/conf/storage-conf.xml 파일에는 default Keyspace1이라는 이름으로 Keyspace가 정의되어 있습니다. Keyspace1에 지정되어 있는 Column Family(CF) 형식은 다음과 같습니다.


Standard2 CF Terry이라는 Key Gender라는 Column Male이라는 값을 넣고 다시 조회해보겠습니다.


다음번에는 Java Code를 이용하여 카산드라에 접근하는 방법에 대해서 알아보도록 하겠습니다.

참고 할만한 자료

지금 하고 있는 숙제가 georeplication system입니다.
Geo replication 시스템이란 분산 시스템중에서 시스템이 여러 IDC (서로 다른 국가나 대륙에 위치)하여 구축되며, 어느 위치에서 접근하던지 같은 데이타를 볼 수 있게 하는 것을 이야기 합니다.
예를 들어 한국 IDC에서 USER정보를 읽어오나, 미국 IDC에서 읽어오나 그 내용이 같다는 겁니다. 물론 USER정보가 계속해서 transaction에 의해서 update되는 상황에서 말입니다. 대충 방향잡고 아키텍쳐 구상도 해놓긴했습니다만, 몇일째 머리가 뽀게지고 있습니다. 다음주에 새로 JOIN하는 호주 친구가 데이타베이스의 전문가니까는 이 부분을 검증하고 보강해주리라 기대하고 있습니다.(돈이 얼만데... -_- 한달 인건비가 거의 일년 연봉... )

그런데 재미있는 것중 하나가 이런 geo replication system이 enterprise world에서는 잘 존재하지 않는다는 겁니다. 정해진 직원수, 정해진 지역에서 서비스하기 때문에 왠만해서는 구경하기 어려운 아키텍쳐입니다. 기껏해야 글로벌 ERP single instance정도겠네요.
반면에 SNS (Social Networking Service)에는 사례가 많습니다. 대표적으로 Facebook, Amazon,Google,Twitter등의 글들이 많이 떠다는데.. 공통점중의 하나는 우습게도 벤더 솔루션을 사용하는 곳은 하나도 없습니다. 다 만들었더군요. Amazone은 Dynamo를 Google은 BigTable은 Facebook은 Cassandra를.. 기술면에서는 이제 벤더들보다 앞서가는게 아닌가 싶습니다.
여기저기 자료 뒤지다가 몇자 긁적여 봤습니다.

근데, Tistory 블로그 왜 자꾸 레이아웃이 깨질까요. ㅜㅡ 텍스트큐브로 옮겨야 하나.

참고로 유용한 URL을 몇개 적어놉니다.
http://highscalability.com/