Express.js 패스포트 및 세션 사용 방법

Express.js 패스포트 및 세션 사용 방법

Express.js에서 패스포트 및 세션 사용 방법에 대해 설명하는 페이지입니다.

Environment

  • Node.js v20.11.1

목차

패스포트(Passport)란?

패스포트(Passport)는 다양한 인증 전략을 간편하게 구현할 수 있게 해주는 인증 미들웨어입니다. 패스포트는 로컬(Local), OAuth, JWT(JSON Web Token) 등 다양한 인증 방식을 지원합니다. 패스포트를 사용하면 인증 로직을 쉽게 분리해서 개발할 수 있습니다.

strategy는 패스포트에서 인증 로직 수행을 담당하는 클래스를 의미하며, 패스포트 사용 시 인증 로직은 Strategy 파일을 생성해서 사용합니다.

세션(Session)이란?

세션(Session)이란 서버 측에서 사용자의 상태를 저장하는 방식으로, 사용자가 웹사이트에 접속할 때 생성된 고유한 세션 ID를 통해 서버에서 사용자를 식별할 수 있습니다. 세션 ID는 보통 쿠키에 저장되어 클라이언트로 전송됩니다.

세션 기반 인증 시스템에서 사용자가 로그인을 하면, 서버는 세션 저장소에 사용자의 정보를 조회하고 세션 ID를 발급합니다. 발급된 ID는 주로 브라우저의 쿠키에 저장합니다. 그 다음에 사용자가 다른 요청을 보낼 때마다 서버는 세션 저장소에서 세션을 조회한 후 로그인 여부를 결정하여 작업을 처리하고 응답을 합니다. 세션 저장소는 주로 메모리, 디스크, 데이터베이스 등을 사용합니다.

세션(Session)은 사용자 인증 및 상태 관리를 위한 방법 중 하나로, 세션을 통해 서버는 사용자의 상태를 유지하고, 로그인 상태나 기타 사용자 정보를 지속적으로 관리할 수 있습니다. 세션을 사용하면 서버 자원을 사용하는 것이므로 서버에 부하를 주는 단점이 있지만, 중요한 정보에 대해 위조, 변조, 탈취가 불가능하므로 보안적인 측면에서 더 안전합니다.

Step 1 - 패키지 설치하기

다음 명령어를 입력하여 passport 라이브러리와 express-session 라이브러리를 설치합니다.

npm install passport passport-local express-session bcrypt mysql2

각 패키지에 대해 설명하자면 다음과 같습니다.

  • passport
    • 패스포트 라이브러리
  • passport-local
    • 유저 아이디와 패스워드로 인증하는 로컬 전략을 사용할 때의 Strategy
  • express-session
    • 세션 저장 라이브러리
  • bcrypt
    • 단방향 해시 함수를 사용한 비밀번호 암호화 라이브러리
  • mysql2
    • MySQL 모듈로, TypeORM과 같은 라이브러리를 사용하셔도 무방합니다.

Step 2 - 패스포트와 세션 설정하기

다음과 같이 app.js에 패스포트와 세션 설정 코드를 추가합니다.

(...)

import session from "express-session";

(...)


// 세션 설정
app.use(
  session({
    secret: process.env.SESSION_SECRET, // 세션 암호화에 사용되는 키
    resave: false, // 세션을 항상 저장할 지 여부
    saveUninitialized: false, // 세션이 저장되기 전에는 초기화되지 않은 상태로 세션을 미리 만들어 저장
    cookie: { maxAge: 1000 * 60 * 60, httpOnly: true }, // 쿠키 유효기간: 1시간
  })
);

// passport 초기화 및 세션 저장소 초기화
app.use(passport.initialize()); // req에 passport 요청을 심음
app.use(passport.session()); // req.session 객체에 passport정보를 추가 저장

(...)

위의 코드를 설명하자면 다음과 같습니다.

  • secret
    • 세션 암호화에 사용되는 키로, 외부로 유출되지 않도록 주의해야 합니다.
  • resave
    • 세션 데이터가 변경되지 않더라도 세션을 다시 저장할지 여부를 나타냅니다.
  • saveUninitialized
    • 초기화되지 않은 세션을 저장할지 여부를 나타냅니다.

주의할 점으로 app.use(session({})) 부분이 패스포트 초기화 부분보다 먼저 작성되어야 합니다. 또한 app.use(session({})) 세션 설정 부분이 다른 미들웨어보다 먼저 설정되어야 합니다.

Step 3 - AuthService와 AuthController 구현하기

다음과 같이 인증 시 사용하는 AuthService를 구현합니다.

import pool from "../../db/db.js";
import bcrypt from "bcrypt";

/**
 * 닉네임과 비밀번호로 사용자 정보를 확인하는 함수
 *
 * @param {string} nickname 닉네임
 * @param {string} password 비밀번호
 * @returns {Promise<{ id: number, nickname: string } | null>}
 */
const validateUser = async (nickname, password) => {
  const [rows] = await pool.execute(
    "select id, nickname as name, hashed_password as hashedPassword from users where nickname = ?;",
    [nickname]
  );

  // 해당 닉네임을 가진 사용자가 없는 경우
  if (rows.length === 0) {
    return null;
  }

  const { id, name, hashedPassword } = rows[0];

  // 비밀번호를 따로 뽑아냅니다.

  bcrypt.compare(password, hashedPassword).then((flag) => {
    if (flag) {
      // 비밀번호가 일치하면 성공
      return {
        id: id,
        nickname: name,
      };
    } else {
      return null;
    }
  });

  if (await bcrypt.compare(password, hashedPassword)) {
    // 비밀번호가 일치하면 성공
    return {
      id: id,
      nickname: name,
    };
  }
  return null;
};

/**
 * 사용자 id 값이 주어졌을 때 사용자 정보를 확인하는 함수
 *
 * @param {number} id 사용자 id 값
 */
const findUser = async (id) => {
  const [rows] = await pool.execute(
    "select id, nickname, hashed_password from users where id = ?;",
    [id]
  );
  return rows[0];
};

다음으로 사용자의 로그인 HTTP 요청을 처리할 AuthController를 구현합니다.

import { request, response } from "express";
import authService from "./auth.service.js";

/**
 * 로그인
 *
 * POST /api/auth/login
 *
 * @param {request} req
 * @param {response} res
 */
const logIn = async (req, res) => {
  res.status(200).json(req.user);
};

위의 코드에서 req.user는 passport를 사용 시 유저 정보를 가져오는 방법입니다.

Step 4 - LocalStrategy 구현하기

다음과 같이 닉네임과 비밀번호로 인증하는 LocalStrategy를 생성합니다.

import passport from "passport";
import LocalStrategy from "passport-local";
import authService from "./auth.service.js";

export const useLocalStrategy = () => {
  passport.use(
    new LocalStrategy.Strategy(
      { usernameField: "nickname", passwordField: "password" },
      async (nickname, password, done) => {
        const user = await authService.validateUser(nickname, password);
        if (!user) {
          return done(null, false, {
            message: "닉네임 또는 비밀번호 정보가 올바르지 않습니다.",
          });
        }
        return done(null, user);
      }
    )
  );
};

LocalStrategy는 기본적으로 인증 시 사용하는 필드명이 username과 password로 정해져 있습니다. 저는 닉네임과 비밀번호로 인증을 할 예정이므로 { usernameField: "nickname", passwordField: "password" }와 같이 usernameField를 nickname으로 변경하였습니다.

추가적으로 위에서 사용한 LocalStrategy 인증 방법 이외에도 다양한 strategy가 있습니다.

인증 방법패키지명설명
Localpassport-local유저명과 패스워드를 사용해 인증
OAuthpassport-oauth페이스북, 구글, 트위터 등의 외부 서비스에서 인증
SAMLpassport-samlSAML 신원 제공자에서 인증, OneLogin, Okta 등
JWTpassport-jwtJSON Web Token을 사용해 인증
AWS Cognitopassport-cognitoAWS의 Cognito user pool을 사용해 인증
LDAPpassport-ldapauthLDAP 디렉토리를 사용해 인증

이외의 인증 방법에 대해서는 다음 링크를 참고하시길 바랍니다.

Passport.js

Step 5 - SessionSerializer 구현하기

다음과 같이 세션에 정보를 저장하거나, 세션에서 정보를 가져오는 SessionSerializer를 생성합니다.

import passport from "passport";
import authService from "./auth.service.js";

export const useSession = () => {
  passport.serializeUser((user, done) => {
    done(null, user.id);
  });

  passport.deserializeUser(async (id, done) => {
    const user = await authService.findUser(id);

    // 사용자 정보가 없는 경우 done() 함수에 에러 전달
    if (!user) {
      done(new Error("No User"), null);
      return;
    }

    // 사용자 정보가 있다면 사용자 정보 반환
    done(null, user);
  });
};

각 메서드에 대해 설명하자면 다음과 같습니다.

  • serializeUser()
    • 세션에 정보를 저장합니다.
  • deserializeUser()
    • 세션에서 가져온 정보로 유저 정보를 반환합니다.

Step 6 - LocalStrategy와 SessionSerializer 설정하기

위에서 작성한 LocalStrategy와 SessionSerializer를 사용하기 위해 app.jsuseLocalStrategy()useSession() 코드를 추가합니다.

// passport 초기화 및 세션 저장소 초기화
app.use(passport.initialize()); // req에 passport 요청을 심음
app.use(passport.session()); // req.session 객체에 passport정보를 추가 저장
useLocalStrategy(); // LocalStrategy 사용
useSession(); // Session 사용

Step 7 - 인증 미들웨어 구현하기

먼저 인증 시 사용할 미들웨어를 구현합니다.

import { request, response } from "express";
import passport from "passport";

/**
 * @param {request} req
 * @param {response} res
 * @param {import("express").NextFunction} next
 */
const authenticate = (req, res, next) => {
  passport.authenticate("local", (authError, user, info) => {
    if (authError) {
      return next(authError);
    }

    if (!user) {
      return res.status(401).json({ message: info.message });
    }

    req.logIn(user, (err) => {
      if (err) {
        console.error(err);
        return res.sendStatus(500);
      }
      next();
    });
  })(req, res, next);
};

Step 8 - Route 설정하기

마지막으로 다음과 같이 route 설정을 진행합니다.

import express from "express";
import authController from "../modules/auth/auth.controller.js";
import authMiddleware from "../modules/auth/auth.middleware.js";

export const router = express.Router();

router.post("/login", authMiddleware.authenticate, authController.logIn);

Comments