passport 로컬, 카카오 적용(1)

복준우·2023년 6월 1일
0

passport

목록 보기
1/2

트립로그 마이그레이션 과정에서 로그인을 처리하기 위해 적용한 passport 사용방법에 대한 포스트입니다.

passport를 사용하여 로컬 로그인을 구현한 후, passport-kakao를 적용하려고 했으나, 카카오 개발자 페이지에서는 카카오 계정(email)의 수집에 대한 검수가 필요하다. 그래서 사용자의 이메일을 수집할 수 없었으며, 닉네임은 필수로 수집할 수 있지만, 내가 개발한 웹사이트에서는 중복된 닉네임 사용을 제한하고 있었기 때문에 카카오 로그인을 사용하는 사용자의 닉네임을 추가로 수집해야 하는 문제가 발생했다. 따라서, 위와 같은 이슈로 인해 passport-kakao를 사용하지 않고, 대신 passport를 사용하여 카카오 로그인을 구현했다.

passport.js를 사용하게 된 이유

  1. 간편한 사용성
    • 간단하고 직관적인 API를 제공하여 사용자 인증을 구현하기 쉽게 해주며, 다양한 인증 전략을 제공하고, 이러한 전략을 조합하여 사용자 인증 로직을 구성할 수 있기 때문에 선택했다.
  2. 다양한 인증 전략
    • 로컬 인증 전략을 통해 사용자 이름과 비밀번호로 인증과 소셜 미디어 인증 중 카카오로그인 전략을 사용하기 위해 선택했다.
  3. 보안 및 전략
    • 보안을 고려하여 설계된 라이브러리이며 사용자 인증 정보를 안전하게 처리하고, 세션 관리 및 비밀번호 해싱과 같은 보안 기능을 제공해 보안에 대한 고려 사항을 신경쓰지 않고, 커스텀 전략을 만들어서 사용자 인증이 필요한 라우터에 전략을 추가해 안전한 인증 시스템을 구축할 수 있기 때문이다.

passport.js란?

Passport.js는 Node.js 기반 웹 애플리케이션에서 사용되는 인증 미들웨어이다. Passport.js를 사용하면 사용자 인증과 관련된 작업을 간단하게 처리할 수 있다.

Passport.js는 다양한 인증 전략(예: 로컬 인증, 소셜 미디어 인증 등)을 제공하여 사용자를 인증하는 방법을 표준화하고, 보안적인 인증 메커니즘을 구현하는 데 도움을 준다. 이를 통해 사용자 인증 로직을 쉽게 구현할 수 있으며, 다른 인증 서비스 (예: Google, Kakao, Twitter 등)와의 통합도 간단히 처리가 가능하다.

Passport.js 주요 개념과 구성 요소

  1. Strategies(전략)
    • passport.js는 다양한 인증 전략을 제공한다. 각 전략은 특정 인증 메커니즘을 구현하며, 해당 전략을 사용하여 사용자를 인증할 수 있다.
  2. Serialize & Deserialize
    • passport.js는 사용자 인증 세션을 관리하기 위해 "serialize"와 "deserialize" 함수를 제공한다. "serialize" 함수는 사용자 객체를 세션에 저장하는 역할을 하며, "deserialize" 함수는 세션에서 사용자 객체를 가져오는 역할을 한다.
  3. Middleware(미들웨어)
    • passport.js는 Express와 함께 사용될 수 있으며, 미들웨어로서 동작한다. 애플리케이션의 라우터 핸들러 이전에 passport.js 미들웨어를 사용하여 사용자 인증을 처리한다.
  4. 인증 요청 및 콜백
    • passport.js는 사용자 인증 요청을 처리하고, 콜백을 통해 인증 결과를 반환한다. 사용자가 로그인 요청에 대한 콜백, 인증에 실패할 때 호출되는 콜백을 구현할 수 있다.

passport 통한 로컬로그인 구현

  1. passport.js와 관련한 패키지를 설치

    npm install passport passport-local express-session
  2. passport 설정하기

    const express = require('express');
    const passport = require('passport');
    const session = require('express-session');
  3. passport 초기화 및 미들웨어 설정하기

    const app = express();
    
    // 세션 설정
    app.use(session({
      secret: 'mysecret', // 세션에 대한 암호화에 사용할 비밀키
      resave: false, // 세션이 수정되지 않은 경우에도 세션을 다시 저장할지를 결정하는 옵션
      saveUninitialized: false // 초기화되지 않은 새로운 세션을 저장할지를 결정하는 옵션
    }));
    
    // Passport 초기화
    app.use(passport.initialize());
    app.use(passport.session());
  4. 로컬 전략 설정하기

    const LocalStrategy = require('passport-local').Strategy;
    
    passport.use(new LocalStrategy(
      (username, password, done) => {
        // 여기에 실제로 사용자 인증을 처리하는 로직을 작성
        // 사용자가 제공한 username과 password를 검증하고, 인증 결과를 done 콜백에 전달
      }
    ));
  5. 사용자 정보 직렬화 및 역직렬화

    passport.serializeUser((user, done) => {
      // 사용자 정보 중 세션에 저장할 필요가 있는 정보를 선택하여 done 콜백에 전달
    });
    
    passport.deserializeUser((id, done) => {
      // 세션에 저장된 사용자 ID를 이용하여 사용자 정보를 조회하고 done 콜백에 전달
    });

코드 적용

serializeUser

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

serializeUser 에서 일반적으로 세션에 저장할 사용자 정보는 최소한으로 유지하는 것이 좋다. 사용자의 모든 정보를 세션에 저장하는 것은 리소스를 많이 사용할 수 있어 사용자의 닉네임(user.nickname)만 세션에 저장한다.

deserializeUser

passport.deserializeUser(async (nickname, done) => {
  try {
    const client = await mongoClient.connect();
    conㅊst userdb = client.db('TripLogV2').collection('user');
    const user = await userdb.findOne({ nickname });
    done(null, user); // req.user
  } catch (error) {
    done(error, null);
  }
});

deserializeUser 에서 세션에 저장된 닉네임(nickname)을 매개변수로 받아 해당 사용자를 MongoDB에서 찾아 반환한다. 반환된 사용자 객체는 req.user에 저장되어 사용이 가능해진다.

전체 코드

폴더구조
/passport/index.js

const passport = require('passport');
const local = require('./local');
const mongoClient = require('../routes/mongo');

module.exports = () => {
  passport.serializeUser((user, done) => {
    done(null, user.nickname);
  });

  passport.deserializeUser(async (nickname, done) => {
    try {
      const client = await mongoClient.connect();
      const userdb = client.db('TripLogV2').collection('user');
      const user = await userdb.findOne({ nickname });
      done(null, user); // req.user
    } catch (error) {
      done(error, null);
    }
  });

  local();
};

로컬 전략

폴더구조
/passport/local.js

const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const mongoClient = require('../routes/mongo');

const crypto = require('crypto');

const verifyPassword = (password, salt, userPassword) => {
  const hashed = crypto
    .pbkdf2Sync(password, salt, 10, 64, 'sha512')
    .toString('base64');

  if (hashed === userPassword) return true;
  return false;
};

module.exports = () => {
  passport.use(
    new LocalStrategy(
      {
        usernameField: 'email',
        passwordField: 'password',
      },
      async (email, password, done) => {
        try {
          // MongoDB에 연결
          const client = await mongoClient.connect();
          const userdb = client.db('TripLogV2').collection('user');

          // 이메일로 사용자 검색
          const user = await userdb.findOne({ email });

          // 사용자가 존재하지 않는 경우
          if (!user) {
            return done(null, false, {
              type: 'login',
              success: false,
              message: '해당 아이디를 찾을 수 없습니다.',
            });
          }

          // 비밀번호 검증
          const passwordCheckResult = verifyPassword(
            password,
            user.salt,
            user.password
          );

          // 비밀번호가 일치하지 않는 경우
          if (!passwordCheckResult) {
            return done(null, false, {
              type: 'login',
              success: false,
              message: '비밀 번호가 틀립니다.',
            });
          }

          // 인증에 성공한 경우 사용자 정보 반환
          return done(null, user);
        } catch (error) {
          console.error(error);
          return done(error);
        }
      }
    )
  );
};

로컬 로그인 처리

// 로컬 로그인(POST)
router.post('/local', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      console.error(err);
      return next(err);
    }

    if (info) {
      return res.send(info);
    }

    return req.login(user, async (error) => {
      if (error) {
        console.error(error);
        return next(error);
      }

      return res.send({
        type: 'login',
        success: true,
        message: '로그인 되었습니다.',
        nickname: user.nickname,
      });
    });
  })(req, res, next);
});
  • 위의 코드는 로컬 로그인을 처리하는 라우터 핸들러이다. '/local' 경로에 POST 요청이 오면 passport.authenticate 함수를 호출하여 사용자 인증을 수행한다.
  • passport.authenticate('local') 함수는 로컬 전략을 사용하여 사용자를 인증하는 역할을 한다. 인증 과정에서는 입력된 이메일과 비밀번호를 확인하여 사용자를 검증하며 인증이 성공하면 콜백 함수가 호출된다.

위의 콜백 함수에서는 다음과 같은 처리를 수행한다.

  1. 에러가 발생한 경우
    • 에러를 콘솔에 기록하고 다음 미들웨어에 에러를 전달
  2. info 객체가 존재하는 경우(로그인 실패)
    • 인증 정보에 관련된 오류 메시지를 응답으로 전송
      즉, 로컬 전략에서 <사용자가 존재하지 않는 경우>, <비밀번호가 일치하지 않는 경우>를 반환해 클라이언트로 보내준다.
  3. 사용자 인증이 성공한 경우(로그인 성공)
    • req.login 함수를 호출하여 사용자를 로그인 상태로 변경
    • 에러가 발생한 경우에는 에러를 콘솔에 기록하고 다음 미들웨어에 에러를 전달
    • 로그인이 성공한 경우에는 응답으로 성공 메시지와 사용자의 닉네임을 전송

적용된 프론트페이지

profile
사람들에게 하나의 문화를 선물해줄 수 있는 프로그램을 개발하고 싶습니다.

0개의 댓글