본문 바로가기

node.js (OctoberSkyJs)

[node.js 따라배우기 05] 인증과 세션, 접근제어 미들웨어

휴우~ 이번엔 조금 텀이 길었죠? 그래도 중간중간 많은 일들이 있었답니다. : ) 


번역도 스터디 맴버분들과 함께 하기 시작했고요, 이런저런 도움도 많이 받고 있습니다. 그리고 꽤 기대해도 좋으실 자료가 곧 공개될 예정입니다. (두둥!) 참! 그리고 이번 파트5는 번역부터 했기 때문에 우선 내용을 공개한 뒤에 계속 조금씩 수정될 것 같습니다. 그리고 파트6도 곧바로 공개될 예정입니다. 


여러가지로 기대해 주세요! (/^_^)/~




 "Webapp을 만들어 봅시다! Nodepad" 파트4에 오신걸 환영합니다. 본 시리즈는 "Node.js"를 이용해서 웹 애플리케이션(이하 웹앱)을 만들어보는 따라배우기(tutorial)시리즈입니다. node.js를 이용해서 웹앱을 만드는 과정을 따라가면서, 자신만의 애플리케이션을 만들 때 접하게 될 모든 영역을 다룰 예정입니다. 

Part 1: 소개 (Introduction)

Part 2: 설치와 애플리케이션 뼈대 만들기 (Installation and Skeleton App)

Part 3: RESTful 메소드와 테스트 (RESTful Methods and Testing)

Part 4: 템플릿, 파셜, 그리고 문서 생성과 수정(Templates, Partials, Creating and Editing Documents)


컴퓨터가 mongo 데몬을 자동으로 띄우지 않는다면 이번 튜터리얼을 시작하기 전에 띄우는 것을 잊지 마세요.

인증(Authentication)

우리는 서비스가 가능한 앱을 만들었습니다. 하지만, 어떤 종류가 되었든 인증시스템이 없다면 쓸 수 없습니다. 대부분의 제품화된 시스템과 클라이언트 프로젝트들은 인증이 필요합니다. 그리고 OpenID와 OAuth같은 흥미로운 시스템들이 있지만, 대부분의 상업 프로젝트들은 자신만의 로그인 시스템을 갖는걸 선호합니다.

보통은 세션으로 이 부분을 처리합니다.

  • 어떤 유저가 유저이름과 패스워드를 입력합니다.
  • 해당 패스워드는 해싱 알고리즘과 소금으로 암호화됩니다. (소금? 하하하!)
  • 암호화된 값을 데이터베이스의 사용자 레코드의 값과 비교합니다.
  • 일치하면, 해당 유저를 확인해주는 세션키가 생성됩니다.
우리는 사용자들과 세션들을 관리하기 위해 다음과 같은 것들이 필요합니다.
  • 데이터베이스에 들어있는 유저들(=유저정보)
  • 로그인한 유저의 아이디를 저장할 수 있는 세션들
  • 패스워드 암호화
  • 유저 로그인이 필요한 경로에 접근을 제한할 방법

익스프레스(Express)에서의 세션들

익스프레스는 데이터 저장 매커니즘으로 지원받는 Connect 세션 미들웨어에 의존합니다. 메모리기반을 비롯하여 connect-redis, connect-mongodb를 포함한 서드파티 저장소들이 존재한다. 대안적인 옵션은 cookie-sessions인데 사용자의 쿠키안에 세션데이터를 저장합니다.

세션은 다음처럼 설정가능합니다.

app.use(express.cookieDecoder());
app.use(express.session());

 
설정 할 때는 이런 설정 옵션들을 정확한 곳에 위치시키는 것이 중요합니다. 그렇지 않으면 세션 변수들이 요청 객체 안에 생기지 않습니다. 저는 설정을 bodyDecoder와 methodOverride 사이에 두었습니다. 전체 소스는 GitHub를 참조하세요.

이제 우리의 HTTP 응답자는 req.session 에 접근할 수 있을겁니다.

app.get('/item', function(req, res) {
  req.session.message = 'Hello World';
});



MongoDB 세션들

npm install connect-mongodb를 이용해서 connect-mongodb 을 설치하세요. connect-mongodb는 다른 세션 스토어(store)와 비슷하게 동작합니다. 애플리케이션 설정하는 동안에 커넥션에 대한 상세한 부분을 구체화 할 필요가 있습니다.

app.configure('development', function() {
  app.set('db-uri', 'mongodb://localhost/nodepad-development');
});
 
var db = mongoose.connect(app.set('db-uri'));
 
function mongoStoreConnectionArgs() {
  return { dbname: db.db.databaseName,
           host: db.db.serverConfig.host,
           port: db.db.serverConfig.port,
           username: db.uri.username,
           password: db.uri.password };
}
 
app.use(express.session({
  store: mongoStore(mongoStoreConnectionArgs())
}));



만약 API 작성자가 연결 옵션에 대한 표준 포맷을 결정할 수 있다면 이 코드의 대부분은 필요치 않게 될 겁니다. 저는 몽구스(Mongoose)로부터 연결에 대한 상세한 부분들을 추출해 내는 함수를 작성했습니다. 본 예제에서, db는 몽구스 커넥션 인스턴스를 담고 있습니다. 몽구스는 URI들을 통해 제공되어야 하는 커넥션 상세정보를 기대합니다. 그리고 저는 그 방식을 좋아하는데요, 왜냐하면 포맷을 기억하기 쉽기 때문입니다. 저는 app.set을 사용해서 환경에 따른 연결 문자열을 저장해왔습니다.

익스프레스 애플리케이션들을 작성할 때 app.set('name', 'value')을 사용하는 건 좋은 생각입니다. 설정 정보에 접근하는 데는 app.get 보다는 app.set('name') 이 사용된다는 것만 기억하세요.

이제는 몽고 콘솔에서 db.sessions.find() 를 실행하면 생성된 세션들이 리턴 될 것입니다.

접근 제어 미들웨어

익스프레스는 로그인 유저들에게 접근을 제어하는 우아한 방법을 제공합니다. HTTP 핸들러들이 정의 될 때, 추가적인 라우트 미들웨어 파라미터들이 지정될 수 있습니다.

function loadUser(req, res, next) {
  if (req.session.user_id) {
    User.findById(req.session.user_id, function(user) {
      if (user) {
        req.currentUser = user;
        next();
      } else {
        res.redirect('/sessions/new');
      }
    });
  } else {
    res.redirect('/sessions/new');
  }
}
 
app.get('/documents.:format?', loadUser, function(req, res) {
  // ...
});



이제 로그인 유저가 필요한 모든 경로에 loadUser 를 추가함으로써 해당 경로의 접근 처리를 할 수 있게 되었습니다. 미들웨어 그 자체는 평범한 경로 파라미터들과, 또한 next를 갖습니다. (next는 특정 로직에 기반한 경로 핸들러를 실행하는데 사용될 수 있습니다.) 우리 프로젝트에서 한 유저는 세션에서 user_id 를 사용해 읽어들입니다. 만약 유저가 간단하게 next 호출로 찾아지지 않을 경우 브라우저가 로그인 화면으로 리다이렉트 됩니다.



RESTful 세션 모델링

저는 문서들과 비슷한 방식으로 세션들을 모델링 했습니다. new, create, 그리고 delete 경로가 있습니다.

 // 세션들
app.get('/sessions/new', function(req, res) {
  res.render('sessions/new.jade', {
    locals: { user: new User() }
  });
});
 
app.post('/sessions', function(req, res) {
  // 유저를 찾아서 현재 유저 세션 변수에 세팅한다.
});
 
app.del('/sessions', loadUser, function(req, res) {
  // 세션 제거
  if (req.session) {
    req.session.destroy(function() {});
  }
  res.redirect('/sessions/new');
});



유저 모델(User Model)

User 모델은 Document 모델보다 좀 더 복잡합니다. 왜냐하면 인증 관련 코드들을 포함해야 하기 때문입니다. 제가 사용한 전략은 이렇습니다. (아마도 이전에 OO 웹 프레임워크들에서 본적이 있으실 겁니다)

  • 패스워드들은 (더 짜지게) 소금(salt)을 곁들여 암호화된 포맷으로 저장된다.
  • 인증은 데이터베이스에 있는 해당 유저의 암호와 암호화된 평문 암호를 서로 비교하는 걸로 처리한다.
  • ‘가상’의 password  속성은 등록 편의와 로그인 폼을 위해 평문 패스워드를 드러낸다.
  • 이 속성은 저장하기 전에 해당 패스워드를 자동으로 암호화하는 셋터(setter)를 가진다.
  • 각각의 이메일 주소가 오직 한 명의 사용자를 위해 쓰이는 걸 확실히 하기 위해 유니크 인덱스를 사용한다.
  • 패스워드 암호는 Node의 기본 암호 라이브러리를 사용한다.

var crypto = require('crypto');
 
mongoose.model('User', {
  methods: {
    encryptPassword: function(password) {
      return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
    }
  }
});



encryptPassword 는 소금을 곁들여 sha-1 해시처리된 패스워드 인스턴스 메소드이다. 이때의 소금은 패스워드 셋터에서 패스워드를 암호화하기 전에 생성된다.

mongoose.model('User', {
  // ...
 
  setters: {
    password: function(password) {
      this._password = password;
      this.salt = this.makeSalt();
      this.hashed_password = this.encryptPassword(password);
    }
  },
 
  methods: {
    authenticate: function(plainText) {
      return this.encryptPassword(plainText) === this.hashed_password;
    },
 
    makeSalt: function() {
      return Math.round((new Date().valueOf() * Math.random())) + '';
    },
 
    // ...



소금은 원하는 뭐든 될 수 있는데, 저는 여기서 상당히 랜덤한 문자열을 생성해냈습니다.

(이크!! ‘소금’이 너무 많이 나와서 농담이 지루해 지려고 하네요 -_- 옮긴이 주)



사용자 저장과 등록

몽구스는 레코드들이 저장될 때 save  메소드 오버라이드를 이용해서 해당 작업들을 편하게 해줍니다.


mongoose.model('User', {
  // ...
  methods: {
    // ...
 
    save: function(okFn, failedFn) {
      if (this.isValid()) {
        this.__super__(okFn);
      } else {
        failedFn();
      }
    }
 
    // ...



저는 실패한 저장 메소드를 허용하기 위해 save 를 오버라이드 해왔습니다. 이렇게하면 실패한 등록의 처리도 좀 더 간단해집니다.


app.post('/users.:format?', function(req, res) {
  var user = new User(req.body.user);
 
  function userSaved() {
    switch (req.params.format) {
      case 'json':
        res.send(user.__doc);
      break;
 
      default:
        req.session.user_id = user.id;
        res.redirect('/documents');
    }
  }
 
  function userSaveFailed() {
    res.render('users/new.jade', {
      locals: { user: user }
    });
  }
 
  user.save(userSaved, userSaveFailed);
});



지금 당장은 아무런 에러 메시지도 표시되지 않습니다. 다른 튜터리얼에서 해당내용을 처리할 예정입니다. 사실 이런식의 유효성검증 은 꽤 바보 같겠지만, 본 애플리케이션에서 인덱스는 매우 중요합니다.


mongoose.model('User', {
  // ...
 
  indexes: [
    [{ email: 1 }, { unique: true }]
  ],
 
  // ...
});



이렇게 하면 유저가 중복으로 저장되는 것을 방지할 겁니다. 포맷은 MongoDB의 ensureIndex와 동일합니다. 


결론

이제 우리는 다음과 같은 것을 가지게 되었습니다.
  • MongoDB 세션들
  • sha1 패스워드 암호를 지원하는 User 모델
  • 문서 접근 제어용 라우팅 미들웨어
  • 유저 등록과 로그인
  • 세션 관리
  • 저는 기본적인 로그인 폼을 포함하도록 Jade 템플릿을 업데이트했습니다.

하지만 그럼에도 몇몇 빠뜨린 것들이 있습니다.
  • 문서 소유자 계정에 대한 처리는 고려하고 있지 않다
  • 테스트가 적절하게 동작하지 않는다. Expresso 테스트에서 세션을 다루는 방법을 이해하는데 어려움을 겪고 있기 때문입니다.

이것들에 대해서는 향후 튜터리얼 시리즈에서 다룰 예정입니다.