잊지 않겠습니다.

nodejs + express를 이용한 Google OAuth 연동

OAuth는 근간에 거의 모든 WebSite에서 사용되고 있는 인증방법입니다. 회원 가입절차를 빠르게 할 수 있으며, 개인정보에 대한 관리 이슈를 피할 수 있어, 많이들 사용되는 방법입니다. nodejs를 이용한 OAuth 인증 방법에 대해서 알아보도록 하겠습니다.

npm package 설치

npm install passport
npm install passport-google-oauth
npm install cookie-session
  • passport: OAuth를 지원하기 위한 base package 입니다. twitter, facebook, google에서 지원하고 있는 OAuth를 모두 지원합니다.
  • passport-google-oauth: passport를 기반으로 하는 google-oauth 지원 package입니다.
  • cookie-session: express에서 session을 지원하기 위한 package입니다. express-session으로 대체해서 사용 가능합니다.

google auth api 설정

https://console.developers.google.com 에서 Google에서 제공하는 API를 이용하는 project를 생성할 수 있습니다. 새로운 project를 생성한 후, API 및 인증 > 사용자인증정보에서 새클라이언트 ID 만들기를 통해서 새로운 API를 만들어줍니다. 여기서 중요한 설정은 다음과 같습니다.

  • 승인된 javascript 원본 : 인증을 요청할 url을 넣습니다. 개발자 환경인 http://localhost:3000 과 같은 표현 역시 가능합니다.
  • 승인된 redirection URL : 인증이 완료된 후, redirect될 url을 넣어줍니다.

만들어진 웹어플리케이션용 Client ID에서 이제 3개의 정보는 우리가 작성할 application에서 사용해야지 됩니다.

  • 클라이언트 ID
  • 클라이언트 보안 비밀
  • URI 리디렉션

Login uri handling

사용자가 OAuth를 통해서 인증될 URL을 설정합니다. express controller code에 해당되는 내용입니다.

module.exports = OAuthController;

function OAuthController(app) {
  var passport = require('passport');
  app.use(passport.initialize());
  app.use(passport.session());
  // Google OAuth를 사용하는데 필요한 객체를 선언합니다.
  var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;

  passport.serializeUser(function(user, done) {
    done(null, user);
  });
  passport.deserializeUser(function(obj, done) {
    done(null, obj);
  });

  passport.use(new GoogleStrategy({
    clientID: '클라이언트 ID',
    clientSecret: '클라이언트 보안 비밀',
    callbackURL: 'URI 리다렉션'
  },
  function(accessToken, refreshToken, profile, done) {
    //
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    // req.session.passport 정보를 저장
    // done 메소드에 전달된 정보가 세션에 저장된다.
    // profile을 이용해서 사용자 정보를 DB에 넣는 등의 작업에 활용할 수 있다.
    // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    //
    return done(null, profile);
  }));

  // 인증 URI : uri link를 이곳에 넣어주면 됩니다.
  app.get('/auth/google', passport.authenticate('google', {
    scope: [
      'https://www.googleapis.com/auth/userinfo.profile',
      'https://www.googleapis.com/auth/userinfo.email'
    ]
  }));

  app.get('/oauth2callback', passport.authenticate('google', {
    successRedirect: '/auth/successed',
    failureRedirect: '/auth/failure'
  }));

  app.get('/logout', function(req, res) {
    //
    // passport 에서 지원하는 logout 메소드이다.
    // req.session.passport 의 정보를 삭제한다.
    //
    req.logout();
    req.session = null;
    res.redirect('/');
  });
};

Summary

Passport를 통해서 Google OAuth에 접근하는 방법은 많은 예시 code들이 있고, 거의 유사한 형태를 갖습니다. 위 controller code를 그대로 copy & paste로만도 간단히 Google OAuth를 구현 가능합니다.


Posted by xyzlast Y2K

jasmine-node를 이용한 nodejs test

설치

다음 npm package를 설치해서 처리한다.

sudo npm install -g jasmine-node

Application의 구성

기본적으로 yo에서 제공되는 application 구조를 그대로 가지고 가는 것이 좋다. (express application을 기준)

.
├── Gruntfile.js
├── app
│   └── lib
├── app.js
├── npm-debug.log
├── package.json
└── test
    └── calculator-spec.js

test code의 실행

일반적으로 root directory에서 다음 명령어를 이용해서 처리하면 된다.

jasmine-node --test-dir test --autotest --watch app test --color

simple test code

target이 되는 file의 위치가 app/lib/testservice.js 라고 할때, export되는 객체는 다음과 같이 구성될 수 있다.

exports.method1 = function() {
  return true;
};

exports.method2 = function() {
  return false;
};

javascript는 method 단위의 구성을 주목할 필요가 있다.

이때, 이 method1, method2를 test하는 code는 다음과 같이 작성가능하다.

var testservice = require('../app/lib/testservice.js');
describe('test testservice', function() {
  it('method1 called', function() {
    expect(testservice.method1()).toBe(true);
  });

  it('method2 called', function() {
    expect(testservice.method2()).toBe(false);
  });
});

simple test code 2

만약에 db를 접속해서 만들어지는 BL이 존재한다면 다음과 같이 구성한다. yo express-generator에서는 db connection이 app.js에서 구성되는데, 이를 따로 뽑아서 사용하면 db에 대한 service code test가 쉽다.
mongoDb에 접근하는 코드는 다음과 같다.

var mongoose = require('mongoose'),
    glob = require('glob'),
    path = require('path'),
    rootPath = path.normalize(__dirname + '/..');

var init = function() {
  mongoose.connect('mongodb://localhost/sadari');
  var db = mongoose.connection;
  db.on('error', function() {
    throw new Error('unable connect to database');
  });

  //glob.sync는 absolute directory를 기준으로 검색하기 때문에 반드시 path에 대한 정보가 필요하다.
  var models = glob.sync(rootPath + '/models/*.js');
  models.forEach(function(model) {
    require(model);
  });
};

exports.init = init;

위와 같은 db connection을 이용하는 간단한 CRUD service를 작성할 때, code는 다음과 같이 구성될 수 있다.

var mongoose = require('mongoose');
var Player = mongoose.model('Player');
var ObjectId = mongoose.Types.ObjectId;

var findAll = function(func) {
  return Player.find(function(err, players) {
    if(func) {
      func(players);
    }
  });
};

var save = function(name, defaultAmount) {
  var player = new Player({
    name: name,
    defaultAmount: defaultAmount
  });
  return player.save(function(err) {
    throw new Exception(err);
  });
};

var findOne = function(id, func) {
  var objectId = mongoose.Types.ObjectId(id);
  Player.findById(objectId, func);
};

var find = function(query, func) {
  Player.find(query, func);
};

var update = function(id, name, defaultAmount, func) {
  var objectId = mongoose.Types.ObjectId(id);
  Player.update(
    {_id: objectId},
    {
      $set: {
        name: name,
        defaultAmount: defaultAmount
      }
    }, { upsert: false, multi: true }, func);
};

exports.update = update;
exports.findOne = findOne;
exports.findAll = findAll;
exports.save = save;
exports.find = find;

이에 대한 test code는 다음과 같다.

var dbConnection = require('../app/lib/db-connect.js');
dbConnection.init();
var playerService = require('../app/lib/playerservice.js');

describe('mongoDb test', function() {
  it('playerService.findAll', function(done) {
    var conn = playerService.findAll(function(players) {
      expect(players.length).not.toBe(0);
      players.forEach(function(player) {
        expect(player.defaultAmount).not.toBe(0);
      });
      done();
    });
    expect(!!conn).toBe(true);
  });

  it('playerService.save', function() {
    var result = playerService.save('playerName', 10);
    expect(result).not.toBe(null);
  });

  it('playerService.find', function(done) {
    playerService.find({}, function(err, players) {
      expect(players.length).toBe(1);
      players.forEach(function(player) {
        console.log(player.name);
      });
      done();
    });
  });

  it('playerService.findOne', function(done) {
    var id = "5469751de83eaf6e34434c5e";
    playerService.findOne(id, function(err, player) {
      console.log(err);
      expect(!!err).toBe(false);
      expect(player).not.toBe(null);
      console.log(player);
      console.log(player.name);

      done();
    });
  });

  it('playerService.update', function(done) {
    var id = "5469751ae83eaf6e34434c55";
    playerService.update(id, "changedName2", 100, function(err, model) {
      expect(!!err).toBe(false);
      done();
    });
  });

  it('players list', function(done) {
    var mongoose = require('mongoose');
    var Player = mongoose.model('Player');
    var playersResult = null;
    Player.find(function(err, players) {
      expect(players.length).not.toBe(0);
      players.forEach(function(p) {
        expect(p.defaultAmount).not.toBe(0);
      });
      done();
    });
  });
});


Posted by xyzlast Y2K

밤새 '민물장어의 꿈'을 계속 들으면서 출근을 했다.


중/고등학교때 음악을 많이 듣는 편은 아니였지만, 나를 힘들게 하던 여러 일들을 잠시 잊게 하고, 나아가는 힘을 주었던것들은 지금 생각해보면 책과 음악, 친구들이였던것 같다. 그 중에서 음악은 015B, NEXT 이정도가 가장 기억에 남는다. (전람회는 015B에서 나중에 파생되는 거나 다름 없으니까....)


그 중 하나가 갔다. 이렇게 갑자기. 그리고 이렇게 허무하게.


나이가 들면서, 그의 음악을 잘 안듣고 있던 것은 사실이지만...' 해에게서 소년에게'와 '먼 훗날 언젠가'는 노래방에서의 단골 주제가가 되었고, 힘들때 다시 한번씩 들어본던 음악이였는데...


잘가요 마왕. 고마웠어요 좋은 음악을 나에게 들려줘서.

그리고 앞으로도 고마울거에요. 당신이 이 음악들을 남겨줘서.


바이바이. R.I.P




Posted by xyzlast Y2K