2025년 1월 3일

김동환·2025년 1월 3일

오늘의 코드

auth.router.js
// 필요한 모듈 및 상수 가져오기
import express from 'express'; // 웹 애플리케이션을 만들기 위한 프레임워크
import bcrypt from 'bcrypt'; // 비밀번호 암호화 및 검증을 위한 라이브러리
import jwt from 'jsonwebtoken'; // JSON Web Token 생성 및 검증을 위한 라이브러리
import { HTTP_STATUS } from '../constants/http-status.constant.js'; // HTTP 상태 코드 상수
import { MESSAGES } from '../constants/message.constant.js'; // 메시지 상수
import { signUpValidator } from '../middlewares/validators/sign-up-validator.middleware.js'; // 회원가입 요청 유효성 검사 미들웨어
import { signInValidator } from '../middlewares/validators/sign-in-validator.middleware.js'; // 로그인 요청 유효성 검사 미들웨어
import { prisma } from '../utils/prisma.util.js'; // 데이터베이스 작업을 위한 Prisma ORM 인스턴스
import {
  ACCESS_TOKEN_EXPIRES_IN, // 액세스 토큰 만료 시간
  HASH_SALT_ROUNDS, // 비밀번호 해시 생성 시 사용하는 솔트 라운드 수
} from '../constants/auth.constant.js';
import { ACCESS_TOKEN_SECRET } from '../constants/env.constant.js'; // JWT 서명에 사용하는 비밀 키

// 인증 관련 라우터 생성
const authRouter = express.Router();

// 회원가입 라우트
authRouter.post('/sign-up', signUpValidator, async (req, res, next) => {
  try {
    const { email, password, name } = req.body; // 요청 본문에서 이메일, 비밀번호, 이름 추출

    // 동일한 이메일이 이미 존재하는지 확인
    const existedUser = await prisma.user.findUnique({ where: { email } });

    // 이메일이 이미 사용 중인 경우, 충돌 상태 코드 반환
    if (existedUser) {
      return res.status(HTTP_STATUS.CONFLICT).json({
        status: HTTP_STATUS.CONFLICT, // HTTP 409 충돌
        message: MESSAGES.AUTH.COMMON.EMAIL.DUPLICATED, // 미리 정의된 오류 메시지
      });
    }

    // 비밀번호를 지정된 솔트 라운드 수로 암호화
    const hashedPassword = bcrypt.hashSync(password, HASH_SALT_ROUNDS);

    // 데이터베이스에 새로운 사용자 생성
    const data = await prisma.user.create({
      data: {
        email, // 사용자 이메일
        password: hashedPassword, // 암호화된 비밀번호
        name, // 사용자 이름
      },
    });

    // 응답 객체에서 비밀번호 제거 (보안을 위해)
    data.password = undefined;

    // 성공 메시지와 생성된 사용자 데이터 반환
    return res.status(HTTP_STATUS.CREATED).json({
      status: HTTP_STATUS.CREATED, // HTTP 201 생성
      message: MESSAGES.AUTH.SIGN_UP.SUCCEED, // 성공 메시지
      data, // 비밀번호를 제외한 사용자 데이터
    });
  } catch (error) {
    // 오류 발생 시 에러 처리 미들웨어로 전달
    next(error);
  }
});

// 로그인 라우트
authRouter.post('/sign-in', signInValidator, async (req, res, next) => {
  try {
    const { email, password } = req.body; // 요청 본문에서 이메일과 비밀번호 추출

    // 데이터베이스에서 이메일로 사용자 검색
    const user = await prisma.user.findUnique({ where: { email } });

    // 제공된 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호를 비교
    const isPasswordMatched =
      user && bcrypt.compareSync(password, user.password);

    // 사용자가 없거나 비밀번호가 틀린 경우, 인증되지 않음 상태 코드 반환
    if (!isPasswordMatched) {
      return res.status(HTTP_STATUS.UNAUTHORIZED).json({
        status: HTTP_STATUS.UNAUTHORIZED, // HTTP 401 인증되지 않음
        message: MESSAGES.AUTH.COMMON.UNAUTHORIZED, // 미리 정의된 오류 메시지
      });
    }

    // JWT에 사용할 페이로드 생성
    const payload = { id: user.id };

    // JWT 생성 (페이로드, 비밀 키, 만료 시간 사용)
    const accessToken = jwt.sign(payload, ACCESS_TOKEN_SECRET, {
      expiresIn: ACCESS_TOKEN_EXPIRES_IN,
    });

    // 성공 메시지와 생성된 액세스 토큰 반환
    return res.status(HTTP_STATUS.OK).json({
      status: HTTP_STATUS.OK, // HTTP 200 성공
      message: MESSAGES.AUTH.SIGN_IN.SUCCEED, // 성공 메시지
      data: { accessToken }, // 생성된 JWT 토큰
    });
  } catch (error) {
    // 오류 발생 시 에러 처리 미들웨어로 전달
    next(error);
  }
});

// 라우터를 내보내 다른 부분에서 사용 가능하도록 설정
export { authRouter };

TIL: Express 기반 인증 라우터 구현

오늘은 Express.js를 사용하여 사용자 인증을 위한 회원가입로그인 기능을 구현하는 과정을 학습했다. 이 과정에서 bcrypt, jsonwebtoken, 및 Prisma를 활용했으며, 주요 작업 내용을 정리한다.


1. 회원가입 기능

라우트 경로: /sign-up

  • 요청 유효성 검사: signUpValidator 미들웨어를 사용해 요청 본문 유효성 검사를 수행.
  • 기능 구현:
    1. 사용자가 입력한 이메일로 데이터베이스에서 기존 사용자 존재 여부 확인.
    2. 동일한 이메일이 이미 존재할 경우 HTTP 409 (Conflict) 상태 코드와 오류 메시지 반환.
    3. 비밀번호를 bcrypt.hashSync로 암호화. (솔트 라운드는 HASH_SALT_ROUNDS 상수 사용)
    4. Prisma를 통해 새로운 사용자 데이터를 데이터베이스에 저장.
    5. 응답 객체에서 비밀번호 제거 후 성공 메시지와 사용자 데이터 반환.
  • 결과: 사용자 생성 성공 시 HTTP 201 (Created) 상태 코드와 함께 성공 메시지 반환.

주요 코드 스니펫:

const hashedPassword = bcrypt.hashSync(password, HASH_SALT_ROUNDS);
const data = await prisma.user.create({
  data: { email, password: hashedPassword, name },
});
data.password = undefined;
return res.status(HTTP_STATUS.CREATED).json({ status: HTTP_STATUS.CREATED, message: MESSAGES.AUTH.SIGN_UP.SUCCEED, data });

2. 로그인 기능

라우트 경로: /sign-in

  • 요청 유효성 검사: signInValidator 미들웨어를 사용해 요청 본문 유효성 검사.
  • 기능 구현:
    1. 데이터베이스에서 이메일로 사용자 검색.
    2. bcrypt.compareSync를 사용해 제공된 비밀번호와 저장된 해시된 비밀번호 비교.
    3. 인증 실패 시 HTTP 401 (Unauthorized) 상태 코드와 오류 메시지 반환.
    4. JWT를 생성하여 클라이언트에 반환. (JWT는 사용자 ID를 페이로드로 포함하고, 만료 시간은 ACCESS_TOKEN_EXPIRES_IN 사용)
  • 결과: 인증 성공 시 HTTP 200 (OK) 상태 코드와 JWT 반환.

주요 코드 스니펫:

const isPasswordMatched = user && bcrypt.compareSync(password, user.password);
if (!isPasswordMatched) {
  return res.status(HTTP_STATUS.UNAUTHORIZED).json({ status: HTTP_STATUS.UNAUTHORIZED, message: MESSAGES.AUTH.COMMON.UNAUTHORIZED });
}
const accessToken = jwt.sign({ id: user.id }, ACCESS_TOKEN_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRES_IN });
return res.status(HTTP_STATUS.OK).json({ status: HTTP_STATUS.OK, message: MESSAGES.AUTH.SIGN_IN.SUCCEED, data: { accessToken } });

3. 사용된 주요 라이브러리

  • bcrypt: 비밀번호 암호화 및 검증.
  • jsonwebtoken: JWT 생성 및 검증.
  • Prisma: 데이터베이스 ORM으로 사용자 데이터를 효율적으로 관리.

4. 보안 고려 사항

  1. 비밀번호는 암호화된 상태로만 저장하며, 원본 비밀번호는 저장하지 않는다.
  2. JWT 생성 시 비밀 키(ACCESS_TOKEN_SECRET)를 사용하며, 만료 시간을 설정하여 보안을 강화한다.
  3. 모든 오류 상황에 대해 명확한 HTTP 상태 코드와 메시지를 반환한다.

5. 향후 개선점

  • 리프레시 토큰 추가: 액세스 토큰 만료 후 재발급을 지원.
  • 계정 잠금: 로그인 시도 실패가 일정 횟수를 초과하면 계정을 잠금.
  • 비밀번호 복구 기능: 비밀번호 재설정 기능 추가.

이번 작업을 통해 인증 관련 REST API를 설계하고 구현하는 방법을 더 깊이 이해할 수 있었다. Express.js의 미들웨어 구조를 활용한 유효성 검사 및 에러 처리 또한 학습의 중요한 부분이었다.

profile
Node.js 7기

0개의 댓글