passport + JWT를 활용한 로그인 구현

캡틴 노드랭크·2021년 8월 30일
2

NodeJS

목록 보기
7/12

기존 로그인시 jsonwebtoken을 활용하여 기존 액세스 토큰과 재발급 토큰으로 구현했었다. 그러나 소셜로그인 기능을 구현하기 위해, 구글 공식 api를 참조해봤지만 작성하는데 어려움이 있었고, 더 간편한 방법을 찾았다 대표적으로 firebase라는 훌륭한 시스템이 존재하지만

나는 passport라는 라이브러리를 사용해보고 싶었다. 이번엔 passport로 로그인 코드와 JWT 토큰을 인증 받는 코드를 작성해보겠다.

그전에 잠깐, Passport가 뭐임?

Passport는 공식홈페이지에서 이렇게 설명하고있다.

Simple, unobtrusive authentication for Node.js


Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.

passport는 여권을 뜻하는데, 여권은 여행용 증서로 다른 국가에 여행할때 자신을 알려주는 증명서이다. 이 라이브러리 passport는 외부인(사용자)이 웹페이지를 이용할 때 로그인, 소셜 로그인을 통해 세션이나 쿠키에 토큰을 발급받고, 인증하는 편리한 라이브러리이다.

passport는 정말 간편한 점이 코드를 한번 익혀놓으면 다른 소셜 로그인 구현에도 거의 복붙 수준으로 간단하고, 쉽게 작성이 가능하다.

일반여권, 관용여권, 외교관여권 등 다양한 여권이 존재하는 것처럼, passport라이브러리도 passport-jwt, passport-facebook등 다양한 종류가 있다.

기존 코드

  register: async (req, res) => {
    const { password, email, username } = req.body;

    const hash = await bcrypt.hashSync(password, 10);
    const newUser = {
      username: username,
      email: email,
      password: hash,
    };
    try {
      await User.create(newUser).then((data) => {
        res.json({ data: data });
      });
    } catch (e) {
      console.log(e);
    }
  },

  login: async (req, res) => {
    const { password, email } = req.body;

    const origin = await User.findOne({ where: { email } });

    if (!origin) {
      return res.status(404).json({
        message: "유저를 찾을 수 없습니다.",
      });
    }

    if (!(await bcrypt.compareSync(password, origin.password))) {
      return res.status(400).json({
        message: "비밀번호가 일치하지 않습니다.",
      });
    }

    // delete data.dataValues.password;
    const accTokens = generateAccessToken({ _id: origin.id });
    const refTokens = generateRefreshToken({ _id: origin.id });
    sendAccessToken(res, accTokens);
    sendRefreshToken(res, refTokens);
  },

기존 코드는 DB의 USER테이블을 조회하고, 중복 여부를 검사해 진행하도록 작성되어있다.

  sendRefreshToken: (res, refTkn) => {
    res
      .cookie("ref_token", refTkn, {
        httpOnly: true,
      })
      .json({ message: "리프레시 토큰 발급 완료" });
  },

  sendAccessToken: (res, accTkn) => {
    // res.json({
    //   token: accTkn,
    //   message: "로그인 성공",
    // });
    res
      .cookie("auth_token", accTkn, {
        httpOnly: true,
      })
      .json({
        maessage: "토큰 발급 완료",
      });
  },

이때 발급받는 Access Tokenauth_token이라는 이름의 쿠키에다 저장하는데, 문제는 accesstoken 만료 시 refreshToken을 가져와 accesstoken으로 활용되어야 한다는 점인데, 아직 실력이 부족하여 그 부분은 진행하지 못하고있다.

아무튼 Passportpassport-jwt를 활용해 토큰 인증 기반 로그인 폼을 작성하려고한다.

Passport 로그인 구현

로그인을 구현하는데 필요한 라이브러리는 다음과 같다.
bcrypt
passport
passport-jwt
passport-local
jsonwebtoken

npm i bcrypt passport passport-jwt passport-local jsonwebtoken --save
으로 한번에 설치해준다.

패스포트 로컬
패스포트 로컬 깃허브
passport-local 사용법을 보면 다음과 같은데..

passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'passwd',
    passReqToCallback: true,
    session: false
  },
  function(req, username, password, done) {
    // request object is now first argument
    // ...
  }
));

첫번째 인자로 옵션과 두번째 인자로 콜백 함수를 적용할 수 있다.

session을 사용하지 않을경우 false로 둘 수 있으며,

passReqToCallbacktrue로 활성화 하게되면 콜백 함수로 전달해준다.

PASSPORT.JS


const GoogleStrategy = require("passport-google-oauth20").Strategy;
const MicrosoftStrategy = require("passport-google-oauth20").Strategy;
const LocalStragegy = require("passport-local").Strategy;
const JWTStrategy = require("passport-jwt").Strategy;
const ExtractJWT = require("passport-jwt").ExtractJwt;
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { User } = require("../models");
require("dotenv").config();
const passportloginVerify = async (username, password, done) => {
    try {
      const user = await User.findOne({ where: { email: username } });

      if (!user) {
        done(null, false, { message: `존재하지 않는 사용자입니다.` });
        return;
      }
      const compareResult = await bcrypt.compare(password, user.password);

      if (compareResult) {
        done(null, user);
        return;
      }

      done(null, false, { reason: "올바르지 않은 비밀번호 입니다." });
    } catch (e) {
      console.log(e);
      done(e);
    }
  };

  let passportConfig = {
    usernameField: "email",
    passwordField: "password",
  };

passport.use(
    "signin",
    new LocalStragegy(passportConfig, passportloginVerify)
  );

우선 전체 코드를 보면 다음과같다.


const LocalStragegy = require("passport-local").Strategy;
const JWTStrategy = require("passport-jwt").Strategy;
const ExtractJWT = require("passport-jwt").ExtractJwt;
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { User } = require("../models");
require("dotenv").config();

필요한 라이브러리를 싹다 긁어 모아준다.

const {Strategy: JWTStrategy, ExtractJwt: ExtractJWT}= require("passport-jwt").Strategy;;

이렇게도 작성이 가능하다.


  let passportConfig = {
    usernameField: "email",
    passwordField: "password",
    session: false,
  };

첫번째 인자로 옵션이 들어간다. 보안상 세션이 더 유리하다고 하지만 AWS 프리티어인 나로서는 약간이나마 부담을 덜기 위해 쿠키로 사용하려고한다..

그렇기 때문에 session: false


const passportloginVerify = async (username, password, done) => {
    try {
      const user = await User.findOne({ where: { email: username } });

      if (!user) {
        done(null, false, { message: `존재하지 않는 사용자입니다.` });
        return;
      }
      const compareResult = await bcrypt.compare(password, user.password);

      if (compareResult) {
        done(null, user);
        return;
      }

      done(null, false, { reason: "올바르지 않은 비밀번호 입니다." });
    } catch (e) {
      console.log(e);
      done(e);
    }
  };

두번째 콜백함수이다.

asyncawait로 비동기로 작성해주었다.

가장 먼저 User테이블을 조회하여 중복된 이메일이 있는지 찾고난 후,

done()을 통해 빠져나온다.

그리고 현재 Input에 입력한 패스워드와 DB에 저장된 유저의 페스워드를 검증한다.

검증된다면, 결과를 반환한다.

마지막으로

passport에 적용한다.

passport.use(
    "signin",
    new LocalStragegy(passportConfig, passportloginVerify)
  );

만약 세션을 사용한다면 아래의 코드를 넣어줘야한다.

passport.serializeUser((user,done)=>{ 
    done(null,user);
});
passport.deserializeUser((user,done)=>{
    done(null,user);
});

Route.js


const router = require("express").Router();

const {
  signin,
  signup,
  logout,
  profile,
} = require("../controller/userController/userController");
const { authorization } = require("../config/JWTConfig");

router.post("/auth/login", signin);
router.post("/auth/register", signup);
router.get("/auth/logout", authorization, logout);
router.get("/auth/profile", authorization, profile);

module.exports = router;

controller/userController.js

  signin: async (req, res, next) => {
    try {
      passport.authenticate("signin", (err, user, info) => {
        if (err || !user) {
          res.status(400).json({ message: info });
          return;
        }

        req.login(user, (err) => {
          console.log(user);
          if (err) {
            res.json({
              message: err,
            });
          }

          User.findOne({
            where: { email: user.email },
          }).then((user) => {
            const token = generateAccessToken({
              id: user.id,
            });
            sendAccessToken(res, token);
          });
        });
      })(req, res, next);
    } catch (e) {
      res.json({
        message: e,
      });
    }
  },

passport.authenticate()signin이라는 전략을 호출했는데 그 이후의 코드를 이렇게 작성해주는게 맞는건지 확실치 않지만 결과는 잘되더라..

 signin: async (req, res, next) => {
    try {
      passport.authenticate("signin", (err, user, info) => {
        if (err || !user) {
          res.status(400).json({ message: info });
          return;
        }

이부분은 passportconfig를 잘못입력했거나, input값이 잘못되면 걸러준다.


 req.login(user, (err) => {
          console.log(user);
          if (err) {
            res.json({
              message: err,
            });
          }

          User.findOne({
            where: { email: user.email },
          }).then((user) => {
            const token = generateAccessToken({
              id: user.id,
            });
            sendAccessToken(res, token);
          });
        });
      })(req, res, next);

DB에 유저를 조회하고, 이메일이 일치한다면, 회원가입할때 자동으로 생성된 고유값 ID(auto increase)를 반으로 토큰을 발급한다.

토큰은 res.cookie를 통해 cookie로 저장된다.

이제 작성이 끝났으니 결과를 보자

????????

뜻밖의 에러

[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

이 오류를 구글링 해봤더니, 서버가 클라이언트에게 두번 이상의 요청을 보낼때 발생하는 에러라고한다..
요청은 겹치지 않은것 같은데 뭔가 이상하다.. 그래서 계속 코드를 찾아봤다.

문제의 원인과 해결

 req.login(user, (err) => {
          console.log(user);
          if (err) {
            res.status(404).json({
              message: err,
            });
          }
          User.findOne({
      

여러차래 수색 끝에 원인을 찾았는데, if(err)문이 passport 전략을 작성한 부분에서 error요청이 겹친거였다.. 그래서 이부분을 지운 최종 로그인 코드는 아래와 같다.


 signin: async (req, res, next) => {
    try {
      passport.authenticate("signin", (err, user, info) => {
        if (err || !user) {
          res.status(400).json({ message: info });
          return;
        }

        req.login(user, (err) => {
          console.log(user);
       
          User.findOne({
            where: { email: user.email },
          }).then((user) => {
            const token = generateAccessToken({
              id: user.id,
            });

            sendAccessToken(res, token);
            // res.json({
            //   auth: true,
            //   token: token,
            //   maessage: "토큰 발급 완료",
            // });

            // res.redirect("/");
          });
        });
      })(req, res, next);
    } catch (error) {
      res.json({
        message: error,
      });
    }
  },
    

결과를 확인해보자

JWT 인증

이번엔 jsonwebtoken을 통해 인증을 받아보자

ref: 회원 로그인 가이드,Passport 공식 웹페이지, 문제의 에러 해결방안

profile
다시 처음부터 천천히... 급할필요가 없다.

0개의 댓글