Passport.js 를 이용한 로컬 로그인, 회원가입 구현(세션인증 방식)

rkdghwnd·2023년 6월 2일
0

Passport.js 란?

Passport.js는 이름과 같이 여권의 역할을 하는 라이브러리로서 일반적으로 인증과정에서 작성해야하는 복잡한 로직을 간단하게 구현하도록 도와준다. 일반적으로 로그인 인증시에 많이 사용되며 로컬로그인, 구글, 페이스북, 카카오, 트위터 등등 여러 소셜로그인에 대한 인증도 지원한다.

전략(Strategy)

Passport.js에서 전략이란 Passport.js에서 제공하는 인증방식을 말한다.
여러가지 전략은 공식문서에서 확인 할 수 있다.

  • 대표적인 전략
    • 로컬 로그인(passport-local) : 로컬DB에서 로그인 인증 방식
    • 소셜 로그인(kakao, google, facebook, twitter...) : 소셜 네트워크 로그인 인증 방식
  • Passport.js 사용시 전체적인 흐름
    • 사용자가 passport에 인증 요청 -> passport는 인증 성공/실패시 어떤 제어를 할지 결정

로컬 로그인, 회원가입 구현하기

npm install passport passport-local express-session

3개의 패키지를 설치한다.

  • passport : passport.js를 사용하기 위한 기본 모듈
  • passport-local : passport.js 로컬 로그인 구현을 위한 모듈
  • express-session : express에서 session을 사용하기 위한 모듈(인증정보를 세션에 저장하기 위해 사용)

구현 전 환경세팅

쿠키와 세션, passport 관련 세팅을 app.js에 설정해주어야 한다.

const cookieParser = require('cookie-parser');
const session = require("express-session");
const cookieParser = require("cookie-parser");
const passport = require("passport");
const passportConfig = require("./passport");

db.sequelize
  .sync()
  .then(() => {
    console.log("db 연결 성공!");
  })
  .catch(console.error);
passportConfig();

app.use(cookieParser(process.env.COOKIE_SECRET));

app.use(
  session({
    secret: process.env.COOKIE_SECRET,
    resave: false,
    saveUninitialized: false,
    proxy: process.env.NODE_ENV === "production",
    cookie: {
      httpOnly: true,
      secure: true,
      domain: process.env.NODE_ENV === "production" && ".devpost.site",
    },
  })
);

app.use(passport.initialize()); // passport 초기화(요청 객체에 passport 설정을 심음)
app.use(passport.session()); // req.session 객체에 passport정보를 추가 저장
// passport.session()이 실행되면, 세션쿠키 정보를 바탕으로 해서 passport/index.js의 deserializeUser()가 실행하게 한다.

// 주의 : passport에서 session을 사용하려면 express-session을 통해 세션을 먼저 req.session을 먼저 생성해야 한다.

cookie-parser

  • 요청과 함께 들어온 쿠키를 해석하여 req.cookies 객체로 만든다.
  • 유효기간이 지난 쿠키는 알아서 걸러낸다.
  • 인자에 비밀키를 넣으면 비밀키 검증을 한다.
  • 그 외 쿠키옵션 설정 가능

express-session 모듈

  • 세션 관리용 미들웨어
  • 세션은 사용자별로 req.session 객체에 유지된다.
  • 세션인증 로그인이나 세션에 특정 데이터를 임시저장하는 경우에 사용된다.
  • 세션 관련 세부 속성 설정 가능
    • secret: 암호화하는데 쓰일 비밀키
    • resave: 세션을 언제나 저장할지 설정함(false면 passport.serializeUser()에 따라 세션저장여부 결정)
    • saveUninitialized: 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
    • proxy: 노드서버가 프록시 뒤에있다면 설정해주어야 하는 옵션
    • cookie: 세션쿠키 관련 설정
      • httpOnly: js로 쿠키에 접근하지 못하도록 함
      • secure: HTTPS 통신에서만 쿠키를 전송하도록 함
      • domain : 특정 도메인에서만 쿠키가 전달되도록 함

passport.initialize()

  • passport를 초기화함(요청 객체에 passport 설정을 심음)

passport.session()

  • req.session 객체에 passport 인증 완료 정보를 저장한다.
  • 세션쿠키 정보를 바탕으로 해서 passport/index.js의 deserializeUser()가 실행하게 한다.

** 주의
req.session 객체는 express-session에서 생성하는 것이므로
express-session 미들웨어를 먼저 열결하고 passport 미들웨어를 연결해야 한다.

passport 초기 로그인 과정

  1. ID와 Password를 포함한 로그인 요청이 라우터로 들어옴
  2. 라우터를 거쳐 passport.authenticate() 호출 -> local.js 실행
  3. LocalStrategy 전략 실행, done()을 호출하면 passport.authenticate() 라우터로 돌아가 다음 미들웨어를 실행
  4. 라우터 로직 이어서 진행, 로그인 성공시 req.login() 호출 -> index.js의 passport.serializeUser() 호출(passport.index.js)
  5. done(null, user.id)로 넘겨주면 여기의 user.id가 req.session.passport.user에 저장,
    두번째 인자의 user.id가 deserializeUser의 첫 번째 매개변수로 이동
  6. passport.deserializeUser()로 바로 넘어가서 sql조회후 done(null, user)으로 넘기면
    req.user에 두번째 인자값(user) 저장, req.login 미들웨어로 다시 되돌아감.
  7. 나머지 미들웨어 로직 실행후 응답을 보내면 세션이 들어있는 쿠키를 브라우저 보낸다.

routes/user.js

const express = require("express");
const router = express.Router(); 
const passport = require("passport");
const { User, Post, Comment, Nested_Comment, Image } = require("../models");

// 로컬 로그인
router.post("/local/auth", isNotLoggedIn, (req, res, next) => {
  passport.authenticate("local", (err, user, info) => {
    // POST /user/auth
    // 매개변수 : passport local.js 에서 done으로 전달된 인자들
    if (err) {
      console.error(err);
      next(err);
    }
    if (info) {
      // 클라이언트 쪽 에러
      console.log(info);
      return res.status(401).send(info.message); // 401 : 허가되지 않음
    }
    // passport 로그인
    // index.js의 serializeUser(user, done) => {...} 실행
    return req.login(user, async (loginErr) => {
      // passport 로그인에서 에러가 날 경우
      if (loginErr) {
        console.error(loginErr);
        return next(loginErr);
      }

      const fullUserWithoutPassword = await User.findOne({
        where: { id: user.id },

        attributes: { exclude: ["password", "createdAt", "updatedAt"] }, // password 제외하고 가져오기
        include: [
          { model: Post, attributes: ["id"] },
          { model: Post, as: "Liked", attributes: ["id"] },
          { model: Post, as: "Bookmarked", attributes: ["id"] },
          { model: Comment, attributes: ["id"] },
        ],
      });

      return res.status(201).json(fullUserWithoutPassword);
    });
  })(req, res, next);
});

module.exports = router;

passport/local.js

const passport = require("passport");
const { Strategy: LocalStrategy } = require("passport-local");
const bcrypt = require("bcrypt");
const { User } = require("../models");
// { 가져올 변수, 새로지을 이름 }

module.exports = () => {
  passport.use(
    new LocalStrategy( // local 전략을 세움
      {
        usernameField: "email",
        passwordField: "password",
        session: true, // 세션에 저장 여부
    	passReqToCallback: false, // 뒤에 콜백함수 형태를 바꿀것인가
      },
      async (email, password, done) => {
        try {
          // 존재하는 사용자인지 확인
          const user = await User.findOne({
            where: { email },
          });
          if (!user) {
            return done(null, false, {
              message: "존재하지 않는 사용자입니다.",
            });
          }
          // 탈퇴 여부
          if (user.withdraw) {
            return done(null, false, { message: "회원 탈퇴한 사용자입니다." });
          }
          // 비밀번호 일치 여부
          const result = await bcrypt.compare(password, user.password);
          if (result) {
            return done(null, user); // 검증 성공
          }
          return done(null, false, { message: "비밀번호가 틀렸습니다." });
        } catch (error) {
          console.error(error);
          return done(error);
        }
      }
    )
  );
};
  • usernameField, passwordField : 아이디와 비밀번호를 전달받은 폼 필드를 설정

    • 예를 들어 위와 같은 로직의 경우 body에 데이터가 {email: 'apple', password: 'pswd'} 이렇게 올 경우 뒤에 오는 콜백함수의 첫번째 매개변수(email)값은 'apple', 두번째 매개변수(password)값은 'pswd'가 된다.
  • session : 세션을 사용 할지에 대한 여부

  • passReqToCallback : true로 설정하면 뒤의 콜백이 (req, id, password, done) => {}; 로 바뀜, req는 express의 req 객체를 가져옴

  • done : 다음 로직으로 넘겨주는 메소드

    • 첫 번째 인자 : DB조회 같은 서버 에러를 넣는 곳(성공한 경우에는 null을 넣는다)
    • 두 번째 인자 : 성공했을때 return할 값을 넣는곳(성공할 경우 유저정보, 실패할 경우 false를 넣는다)
    • 세 번 째 인자 : 사용자가 임의로 만드는 에러 메시지, 객체 형태로 작성

passport/index.js

const passport = require("passport");
const { User } = require("../models");
const local = require("./local");
const kakao = require("./kakaoStrategy");
const facebook = require("./facebookStrategy");
const google = require("./googleStrategy");

module.exports = () => {
  passport.serializeUser((user, done) => {
    // Strategy 성공 시 호출됨
    // 여기의 user.id가 req.session.passport.user에 저장
    done(null, user.id); // 여기의 user.id가 deserializeUser의 첫 번째 매개변수로 이동
  });

  passport.deserializeUser(async (id, done) => {
    try {
      // 매개변수 id는 serializeUser의 done의 인자 user.id를 받은 것
      // 매개변수 id는 req.session.passport.user에 저장된 값
      // id 값으로 사용자인증 (서버로 들어오는 매 요청마다 실행)
      const user = await User.findOne({ where: { id } });
      done(null, user); // 여기의 user가 req.user가 됨
    } catch (error) {
      console.error(error);
      done(error);
    }
  });

  local();
};

passport 로그인 이후 과정

  1. app.use(passport.session()) 로 인해 매 요청시 passport.deserializeUser() 메소드를 호출한다.
  2. deserializeUser() 에서는 req.session 에 저장된 id로 사용자 조회(await User.findOne({ where: { id } });)
  3. 조회에 성공하면 done(null, user)로 넘겨주고 req.user에 user 값 저장
  4. 모든 라우터에서 req.user를 공용적으로 사용 가능하게 된다.
    1. 로그인 유지를 위한 라우터를 구현 할 때 req.user를 활용하게 된다.

routes/user.js

// 로그인 정보 유지
router.get("/me", async (req, res, next) => {
  // GET /user
  try {
    if (req.user) {
      const fullUserWithoutPassword = await User.findOne({
        where: { id: req.user.id },

        attributes: { exclude: ["password", "createdAt", "updatedAt"] }, // password 제외하고 가져오기
        include: [
          { model: Post, as: "Liked", attributes: ["id"] },
          { model: Post, attributes: ["id"] },
          {
            model: Comment,
            attributes: ["id"],
          },
          {
            model: Nested_Comment,
            attributes: ["id"],
          },
          { model: Post, as: "Bookmarked", attributes: ["id"] },
        ],
      });
      return res.status(201).json(fullUserWithoutPassword);
    } else {
      res.status(200).json(null);
    }
  } catch (error) {
    console.error(error);
    next(error);
  }
});
profile
rkdghwnd's dev story

0개의 댓글