2018년 4월 12일 목요일

Node.js - 스터디 3주차 - 사용자 관리 (인증)

https://opentutorials.org/course/2136/
생활코딩 강의를 이용해 진행하는 스터디 3주차.

4/13(금) :: 30강 'Multi user(다중 사용자)' 부터 33강 'Federation Authentication(타사 인증)' 까지.


Multi user (다중 사용자)


회원 가입과 로그인을 구현한다. 강의에선 MySQL과 OrientDB를 모두 다루고 있기 때문에 우선 파일 시스템으로 애플리케이션을 구현하는 예를 보여주고 있다. 1) register form을 통해 입력되는 사용자 가입 정보를 global 배열에 집어넣고, 2) 로그인 요청이 있으면 global 배열에서 일치하는 정보가 있는지 확인한 후 welcome 페이지를 띄우는 예제다.

전반적인 흐름에 대한 내용을 파악하는 것이 이 강의의 목적이므로 엄격한 수준의 validation이나 중복 처리 같은 예외상황 처리는 하지 않는다.

배열을 선언하고,
var users = [];

사용자 정보를 추가한다.
var user = {
  username: req.body.username,
  password: req.body.password,
  displayName: req.body.displayName
};
users.push(user);

메모리에 임시적으로 기록되는 정보이기 때문에 서버가 꺼지면 정보는 모두 날아간다. 영속성을 보장하기 위해선 데이터베이스와 같은 별도의 스토리지가 필요한데 이러한 부분은 뒷 강의에서 나오는 것 같다.


Security - Password (비밀번호 보안)


사용자의 비밀번호가 노출되어 악의적인 목적을 가진 사람의 손으로 넘어간다면 큰 문제가 된다. 지속가능한 서비스를 위해 사용자의 비밀번호는 외부에 유출되어선 안되고, 최악의 상황에 대한 대비가 되어 있어야 한다. 최악의 상황에 대한 대비란 얘기는 사용자 비밀번호를 평문 형태로 스토리지에 보관하지 않는 것을 기본으로 한다.

일반적으로 사용자의 비밀번호는 암호화 알고리즘을 통해 암호화된 값으로 스토리지에 보관한다. 그리고 비밀번호 비교 과정에서도 사용자의 입력 비밀번호를 동일한 방식으로 암호화한 후, 결과 해시값 비교를 통해 유효성을 판단한다.

단방향 암호화 알고리즘인 sha256을 사용해보자.
npm install sha256

> var sha256 = require('sha256');
> sha256('javascript');
'eda71746c01c3f465ffd02b6da15a6518e6fbc8f06f1ac525be193be5507069d'

sha256을 이용해 'javascript'를 암호화하면 항상 위와 같은 결과를 얻게된다. 'javascript'를 sha256으로 암호화 했을 때의 결과 값이 'eda71746c01c3f465ffd02b6da15a6518e6fbc8f06f1ac525be193be5507069d'인 것을 알더라도 'eda71746c01c3f465ffd02b6da15a6518e6fbc8f06f1ac525be193be5507069d'만 보고 원문이 'javascript'라는 것을 알기는 매우 어렵다. 사용자가 최초 입력한 원문에 대해 보안을

단순히 암호화 알고리즘을 사용해 정보 보안을 제공하는 방법 말고도 다양한 응용 기법이 존재한다. 대표적으로 암호화된 해시 값을 다시 N번 암호화하는 key stretching, 평문에 임의의 문자열을 추가해서 암호화 하는 salting 등이 있다. (http://d2.naver.com/helloworld/318732 를 참고하자.)

salting은 비밀번호를 암호화하는데 양념을 치는 방법이다.
> var salt = 'asofih23ihaog14oih';
> var password = 'javascript';
> sha256(password + salt);

위와 같이 임의의 문자열을 암호화 과정에 포함시키면 결과 해시 값을 알기 더 어려워진다. 추가로 사용자마다 임의의 salt 값을 부여한다면 결과를 추론하기 더 어려워지기 때문에 더 향상된 수준의 보안을 제공할 수 있게 된다.

Node.js에선 pbkdf2-password란 모듈이 제공된다. 앞서 언급된 내용을 모듈로 구현해 제공하는 것인데 구현간 발생할 수 있는 실수를 상당부분 줄여준다.
npm install pbkdf2-password --save
(사용법: https://www.npmjs.com/package/pbkdf2-password)

! sha256 모듈과 다르게 암호화 결과를 callback으로 제공한다는 점에 주의가 필요하다. node.js는 비동기 시스템이므로 callback이 호출되기 전에 redirect가 동작하는 등의 의도치 않은 결과가 나타날 수 있다.

1) 사용 선언
var bkfd2Password = require("pbkdf2-password");
var hasher = bkfd2Password();

2) 사용 예 - 사용자 등록

3) 사용 예 - 사용자 로그인


Passportjs




최근의 대다수 웹 애플리케이션은 자체 인증 기능만을 제공하지 않고, 페이스북, 구글, 트위터와 같은 서비스에 이미 가입되어 있는 정보를 이용해 사용자를 인증시켜주기도 한다. OAuth란 표준이 있는 것으로 알고있는데 강의에선 Federation Authentication이라 표현하고 있다. 아무튼 passport.js는 사용자 인증과 관련된 다양한 기능을 쉽게 구현할 수 있도록 지원한다. 물론 자체 인증 기능만 사용할 것이라면 굳이 passport.js를 사용할 필요는 없다.

Passport.js를 설치하자.
npm install passport passport-local --save

passport-local를 설치하는 이유는 자체 인증 기능까지 passport.js가 제공하는 방식(style)으로 처리하기 위함이다. (http://www.passportjs.org/docs/username-password/)

우선 미들웨어 사용 선언을 한다. 주의할 점은 session 모듈의 초기화를 마친 다음에 passport.session()를 호출해 주어야 올바르게 동작한다.


passport를 사용하면 기존 인증 기능에 대한 처리는 passport 방식으로 다시 수정해 주어야 한다. passport에서 특정 로그인 방식은 'strategy'란 개념으로 정리된다. 그리고 자체 인증 방식은 'local'이라는 이름으로 표현된다. 기존 예제에선 app.post('/auth/login') 내부에서 로그인과 관련된 모든 과정을 처리했었지만, passport 버전에선 passport의 authenticate 메서드로 처리를 위임한다. 인자로 strategy 이름과 redirect 경로에 대한 정보 등을 받는다.

당연히 위와 같이 하기만하면 로그인이 될 순 없고, 'local' 로그인 전략에 대한 처리 메서드를 직접 구현해야 한다. LocalStrategy란 객체를 선언하면서 처리 메서드를 하나 지정해주는 것인데 건네받는 인자로 username, password, 그리고 로그인 성공 결과 정보를 받는 done이란 callback 함수가 있다.

username, password에 form에서 입력한 값이 바로 꽂히는게 신기했는데, form에서 id가 username, password로 지정된 경우에 한해서만 값을 찾을 수 있다. form에서 지정한 id가 다르다면

passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'passwd'
  },
  function(username, password, done) {
    // ...
  }

));
와 같이 직접 지정해주어야 한다. 전체 모습은 아래와 같다.

done의 첫 번째 인자는 에러 정보, 두 번째 인자는 로그인에 성공한 사용자 객체 또는 false 값이다. 전체적인 chaining은 잘 모르겠지만 어쨌든 done에 전달한 사용자 객체는 serialize되어 세션에 기록된다. 이 부분 또한 사용자가 직접 구현해 주어야 한다. 구현해야할 것은 세션에 값을 기록할 때 id 값을 지정해주기 위한 serializeUser와 세션에 기록된 값을 참조하기 위한 과정인 deserializeUser의 두가지 함수인데 크게 복잡하진 않다. 아래와 같이 unique 성질을 갖는 property를 serializeUser에 넣어주면 deserializeUser에선 그 property를 이용해 원하는 세션 데이터를 찾을 수 있는 구조다.



이렇게 구현이 진행되는 과정에서 자연스레 'req.session.displayName'과 같이 참조했었던 기존 예제가 passport 스타일로 변경된다. 영향을 받는 '/welcome' 페이지와 '/auth/logout'의 변경은 다음과 같다.




Federation Authentication (타사 인증)


Facebook 예. (http://www.passportjs.org/docs/facebook/)
npm install passport-facebook

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

1) Facebook strategy 구현.
passport.use(new FacebookStrategy({
    clientID: FACEBOOK_APP_ID,
    clientSecret: FACEBOOK_APP_SECRET,
    callbackURL: "http://www.example.com/auth/facebook/callback"
  },
  function(accessToken, refreshToken, profile, done) {
    User.findOrCreate(..., function(err, user) {
      if (err) { return done(err); }
      done(null, user);
    });
  }
));
→ profile: Facebook에 등록된 User 정보가 담긴 객체

2) 인증 요청을 Facebook으로 redirect.
// Redirect the user to Facebook for authentication.  When complete,
// Facebook will redirect the user back to the application at
//     /auth/facebook/callback
app.get('/auth/facebook', passport.authenticate('facebook'));

3) 인증 결과에 대한 redirect.
// Facebook will redirect the user to this URL after approval.  Finish the
// authentication process by attempting to obtain an access token.  If
// access was granted, the user will be logged in.  Otherwise,
// authentication has failed.
app.get('/auth/facebook/callback',
  passport.authenticate('facebook', { successRedirect: '/welcome',
                                      failureRedirect: '/auth/login' }));

동작하는 순서는 2) → 1) → 3).

댓글 없음:

댓글 쓰기