passport, localStrategy의 동작에 대해 이해하기

@p-iknow 🎹 · November 07, 2019

들어가며

프론트 개발을 공부하다 처음으로 백엔드 코드를 작성하기 시작했다. 로그인을 구현하기 위해 passport 를 사용해야 했다. 가려진 부분이 많아 해당 패키지를 이용할 때 무슨 일이 일어나는지 알수가 없었다. 필자 처럼 passport 의 마법 같은 인증 로직 처리에 당황할 사람들을 위해 아래에 passport local 전략에 대해 정리했다. 참고로 생활코딩에 갓고잉님의 passport 강좌가 마련되어 있다. 보다 나은 이해를 위해 참고 부탁드린다.

pssport 의 역할

passport는 한마디로 인증을 편하게 관리하기 위한 패키지이다. passport 가 실제로 하는 일은 session 객체 내부에 passport 프로퍼티를 만들고, 값으로 쿠키와 식별자를 매칭해서 저장한다(serialize). 이후 매 요청시에 세션에 저장된 식별자를 이용해 유저의 데이터를 찾아 express 라우터 콜백함수의 request.user 에 해당 데이터를 저장한다(deserialize).

https://velog.io/@ground4ekd/nodejs-passport 참고

passport localStrategy 실행순서

image

실행순서를 정리하면 다음과 같다. login 을 담당하는 라우터의 콜백함수에 의해 passport.authenticate() 메소드가 실행된다. 해당 메소드를 통해 LocalStrategy 생성자에 전달된 callback 함수가 실행되고, 그 후에 passport.serializeUser() 가 실행된다. 해당 메소드 실행시 session 객체 내부 passport 프로퍼티에 cookie 와 식별자를 매칭시켜 보관하고, 추후 매 요청시 마다 passport.deserializeUser() 가 실행되어 session 객체에 저장된 식별자를 통해 user에 대한 데이터를 찾아 req.user에 넣어준다.

자세한 로직은 아래 코드로 설명한다.(예제코드 깃헙)

router 에서 passport.authenticate(local, callback) 실행된다.

const express = require('express');
const bcrypt = require('bcrypt');
const passport = require('passport');
const db = require('../models');

router.post('/login', (req, res, next) => {

  // 이 부분 실행
  passport.authenticate('local', (err, user, info) => {
    console.log(err, user, info);
    if (err) {
      console.error(err);
      return next(err);
    }
    if (info) {
      return res.status(401).send(info.reason);
    }
    return req.login(user, loginErr => {
      if (loginErr) {
        return next(loginErr);
      }
      const fillteredUser = { ...user.dataValues };
      console.dir(fillteredUser);
      delete fillteredUser.password;
      return res.json(fillteredUser);
    });
  })(req, res, next);
});

strategy callback 실행되고 done(err, user, info) 함수에 전달된 인자들이 passpor.authenticate('local' (err, user, info) => {...}) 의 인자로 전달됨, 단 passport.authenticate 의 인자로 custom callback을 사용할 경우에만 이를 확인 가능하고, 나머지의 경우 passport.authenticate 함수가 실행될 때 자동으로 req.login(user, callback)req.login 공식문서 을 실행시켜 serealizeUser(user, done) => {...} 의 인자로 전달된다 )

const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const bcrypt = require('bcrypt');
const db = require('../models');

module.exports = () => {
  passport.use(
    new LocalStrategy(
      /*
      {
        userId: ...,
        password: ...
      }
      */
      {
        usernameField: 'userId',
        passwordField: 'passowrd'
      },
      async (userId, password, done) => {
        try {
          const user = await db.User.findOne({
            where: { userId }
          });
          if (!user) {
            return done(null, false, { reason: '존재하지 않는 사용자입니다!' });
          }
          const result = await bcrypt.compare(password, user.password);
          if (result) {
            return done(null, user);
          }
          return done(null, false, { reason: '비밀번호가 틀립니다.' });
        } catch (e) {
          console.log(e);
          return done(e);
        }
      }
    )
  );
};

pssport.authenticate 의 콜백 (err, user, info) => {...} 이 실행되며, callback 내부의 req.login(user, loginErr => {...}) 실행, 현재는 위에서 언급했던 authenticate의 custom callback을 활용하기 때문에

router.post('/login', (req, res, next) => {
  // POST /api/user/login
  passport.authenticate('local', (err, user, info) => {  // 이 부분 실행
    console.log(err, user, info);
    if (err) {
      console.error(err);
      return next(err);
    }
    if (info) {
      return res.status(401).send(info.reason);
    }
   // req.login 실행
    return req.login(user, loginErr => {
      if (loginErr) {
        return next(loginErr);
      }
      const fillteredUser = { ...user.dataValues };
      console.dir(fillteredUser);
      delete fillteredUser.password;
      return res.json(fillteredUser);
    });
  })(req, res, next);
});

req.login 의 실행으로 sequalizeUser((user, done )=> {...}) 의 인자로 전달된 callback 이 실행되며

const passport = require('passport');
const db = require('../models');
const local = require('./local');

passport.serializeUser((user, done) => { // 이 부분 실행
    return done(null, user.id);
  });

done(null, user.id) 의 결과로 아래의 세션 객체가 생성된다.

{
  "cookie": {
    "originalMaxAge": null,
    "expires": null,
    "secure": false,
    "httpOnly": true,
    "path": "/"
  },
  "passport": { "user": 2 },
  "__lastAccess": 1573025918607
}

passport.serializeUser 의 callback 함수의 done 이 실행되고 난 뒤에는 req.login( user, (loginErr) => {..}) 에 인자로 전달된 callback 함수가 실행되고 해당 함수 내부에서 res.json(fillteredUser) 을 통해 프론트에 필요한 정보를 전달하고 서버의 로직이 종료된다.

router.post('/login', (req, res, next) => {
  console.log('라우터 시작 ');
  // POST /api/user/login
  passport.authenticate('local', (err, user, info) => {
    console.log('passport.authenticalte callback ');
    if (err) {
      console.error(err);
      return next(err);
    }
    if (info) {
      return res.status(401).send(info.reason);
    }
    return req.login(user, loginErr => { // 이 부분 callback 실행
      console.log('req.login callback');
      if (loginErr) {
        return next(loginErr);
      }
      const fillteredUser = { ...user.dataValues };
      delete fillteredUser.password;
      return res.json(fillteredUser);
    });
  })(req, res, next);
});

이후 매 요청시에 passport.deserializeUser 메소드가 실행되는데 이 때 callback 함수의 첫번째 인자로 전달되는 id 값은 passport.serializeUser 의 callback 함수의 내부에서 실행된 done(null, userId) 의 두번째로 전달된 userId 값이다. (내부적으로는 session 객체 내부의 passport 프로퍼티에서 읽어온 것이다.) , deserializeUser 의 callback 함수는 전달된 식별자 값인 id 를 통해 user 정보를 조회하고 이를 req.user(request 객체의 user property) 에 저장해 준다.

const passport = require('passport');
const db = require('../models');
const local = require('./local');

passport.deserializeUser(async (id, done) => {
    try {
      const user = await db.User.findOne({
        where: { id }
      });
      return done(null, user); // 이 때 req.user에 유저 정보 저장
    } catch (e) {
      console.error(e);
      return done(e);
    }
  });
  local();
};
@p-iknow 🎹
많은 것을 이해하고 싶습니다. 더 이해하기 위해 노력합니다.