Node 강의/숙련

2-1 [게시판 프로젝트] 인증 인가 (로그인, 회원 가입 API)

kagan-draca 2024. 9. 8. 17:24

1. 인증, 인가 살펴보기

 

1) 인증(Authentication)

인증(Autentication)은 서비스를 이용하려는 사용자가 인증된 신분을 가진 사람이 맞는지 검증하는 작업

 

ex) 로그인 기능!!!

 

로그인 기능은 일반적으로 id, password의 조합으로 이뤄집니다.

 

2) 인가(Authorization)

 

 

인가(Authorization)는 이미 인증된 사용자가 특정 리소스에 접근하거나 특정 작업을 수행할 수 있는

권한이 있는지 검증하는 작업을 뜻 합니다.

 

인증된 사용자 즉, 로그인 된 사용자만 게시글을 작성할 수 있는지 검증한다면,

인가(Authorization) 과정이라고 부릅니다.

 

인가(Authorization) 기능사용자 인증 미들웨어를 통해서 구현할 예정 입니다.

 

2. 로그인, 회원가입 API 만들기

 

 API 명세서

 

1 ) 밑 작업하기

 

src/app.js

import express from "express";
import cookieParser from "cookie-parser";

const app = express();
const PORT = 3018;

app.use(express.json());
app.use(cookieParser());

app.listen(PORT, () => {
  console.log(PORT, "프트로 서버가 열렸습니다");
});

 

src/utils/prisma/index.js

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient({
  log: ["query", "info", "warn", "error"],
  errorFormat: "pretty",
});

export default prisma;

 

2 ) 회원 가입 API

회원가입 API 비즈니스 로직

 

1. email, password, name, age, gender, profileImage를 Body로 전달받습니다.

2. 동일한 email을 가진 사용자가 있는지 확인합니다.

3. Users 테이블에 email, password를 이용해 사용자를 생성합니다.

4. UserInfo 테이블에 name, age, gender, profileImage를 이용해 사용자 정보를 생성합니다.

 

회원가입 API는 사용자와 사용자 정보가 1 : 1 관계를 가진 것을 바탕으로 로직을 구현한다.

 

사용자 → 사용자 정보 순으로 회원가입을 진행한다.

 

routers/users.router.js 파일 생성, UsersRouter를 app.js 전역 미들웨어에 등록 후 회원가입 API를 구현한다.

 

// 회원가입 API
UsersRouter.post("/sign-up", async (req, res, next) => {
  const { email, password, name, age, gender, profileImage } = req.body;
  const isExistUser = await prisma.Users.findFirst({
    where: {
      email,
    },
  });
  if (isExistUser)
    return res.status(409).json({ message: "이미 존재하는 이메일 입니다." });

  // User 테이블에 값 넣기
  const user = await prisma.Users.create({
    data: {
      email,
      password,
    },
  });

  // UserInfos 테이블에 값 넣기
  const userInfo = await prisma.UserInfos.create({
    data: {
      userId: user.userId,
      //생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
      name,
      age,
      gender: gender.toUpperCase(), //성별 대문자로 변환
      profileImage,
    },
  });
  return res.status(201).json({ massage: "회원가입에 성공했습니다" });
});

 

localhost:3018/api/sign-up으로 회원가입을 진행하면,

Client에게 회원가입이 정상적으로 응답이 옵니다.

 

만약, email이 같은 상황에서 한 번 더 보내면,

 

 

이미 존재하는 이메일이라는 문구가 Client에게 응답으로 옵니다.

 

3 ) 개인정보 암호화 하기(bcrypt)

 

일반적으로, 사용자의 개인정보를 데이터베이스에 저장할 때,

보안을 위해 비밀번호를 평문으로 저장하지 않고 암호화 하여 저장합니다.

 

ex) 주민등록번호, 비밀번호

 

bcrypt 모듈이란?

 

bcrypt 모듈은 입력받은 데이터를 특정 암호화 알고리즘을 이용하여 암호화 및 검증을 도와주는 모듈입니다.

 

bcryt를 이용해 암호화하게 된다면 특정한 문자열로 반환됩니다.

이 변환된 문자열은 단방향 암호화 돼 원래의 비밀번호로 복구할 수 없습니다.

 

하지만, 암호화된 비밀번호가 사용자의 입력과 일치하는지는 비교할 수 있습니다.

 

이를 통해 사용자의 비밀번호가 올바른지, 아닌지 검증할 수 있게 됩니다.

 

bcrypt 암호화


import bcrypt from 'bcrypt';

const password = 'Sparta'; // 사용자의 비밀번호
const saltRounds = 10; // salt를 얼마나 복잡하게 만들지 결정합니다.

// 'hashedPassword'는 암호화된 비밀번호 입니다.
const hashedPassword = await bcrypt.hash(password, saltRounds);

console.log(hashedPassword); //$2b$10$OOziCKNP/dH1jd.Wvc3JluZVm7H8WXR8oUmxUQ/cfdizQOLjCXoXa

 

 

bcrypt 복호화

const password = 'Sparta'; // 사용자가 입력한 비밀번호
const hashed = '$2b$10$OOziCKNP/dH1jd.Wvc3JluZVm7H8WXR8oUmxUQ/cfdizQOLjCXoXa'; // DB에서 가져온 암호화된 비밀번호

// 'result'는 비밀번호가 일치하면 'true' 아니면 'false'
const result = await bcrypt.compare(password, hashed);

console.log(result); // true

// 비밀번호가 일치하지 않다면, 'false'
const failedResult = await bcrypt.compare('FailedPassword', hashed);

console.log(failedResult); // false

 

4 ) 회원 가입 API 리팩토링(개인정보 암호화하기)

 

yarn add bcrypt

#yarn을 이용해서 bcrypt를 설치합니다.

 

만약, /bin/sh : node-pre-gyp: command not found 에러 메시지가 출력된다면,

 

npm install -g node-pre-gyp

# node-pre-gyp 패키지를 전역으로 설치합니다.

 

이제 본격적으로 리팩토링 해보겠습니다.

 

  const hashedPassword = await bcrypt.hash(password, 10);

를 isExistUser가 존재하는지 확인하는 if문이 끝나는 위치에 추가해줍니다.

  // User 테이블에 값 넣기
  const user = await prisma.Users.create({
    data: {
      email,
      password : hashedPassword
    },
  });

그리고 User 테이블에 password를 

 

password : hashedPassword

 

로 바꿔줍니다.

 

// 회원가입 API
UsersRouter.post("/sign-up", async (req, res, next) => {
  const { email, password, name, age, gender, profileImage } = req.body;
  const isExistUser = await prisma.Users.findFirst({
    where: {
      email,
    },
  });
  if (isExistUser)
    return res.status(409).json({ message: "이미 존재하는 이메일 입니다." });

  const hashedPassword = await bcrypt.hash(password, 10);

  // User 테이블에 값 넣기
  const user = await prisma.Users.create({
    data: {
      email,
      password : hashedPassword
    },
  });

  // UserInfos 테이블에 값 넣기
  const userInfo = await prisma.UserInfos.create({
    data: {
      userId: user.userId,
      //생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
      name,
      age,
      gender: gender.toUpperCase(), //성별 대문자로 변환
      profileImage,
    },
  });
  return res.status(201).json({ massage: "회원가입에 성공했습니다" });
});

 

 

새로운 유저를 회원가입시켜주고

 

Users 테이블을 확인해보면,

 

 

password가 암호화된걸 확인할 수 있습니다.

 

5 ) 로그인 API, 사용자 인증 미들웨어

 

로그인 API 만들기

 

로그인 API 비즈니스 로직

  1. email, password를 body로 전달받습니다.
  2. 전달 받은 email에 해당하는 사용자가 있는지 확인합니다.
  3. 전달 받은 password와 데이터베이스의 저장된 password를 bcrypt를 이용해 검증합니다.
  4. 로그인에 성공한다면, 사용자에게 JWT를 발급합니다. </aside>
UsersRouter.post("/sign-in", async (req, res, next) => {
  const { email, password } = req.body;
  const user = await prisma.Users.findFirst({
    where: {
      email,
    },
  });
  if (!user)
    return res.status(409).json({ massage: "이메일이 존재하지 않습니다" });

  if (!(await bcrypt.compare(user.password, password)))
    return res.status(401).json({ massage: "비밀번호가 일치하지 않습니다." });

  const token = jwt.sign({ userId: user.userId }, "custom-secrect-key");
  // 암호화할 내용, 암호화에 사용될 키 이름(향후 복호화할 때 비교 대상으로 사용)
  res.cookie("authorization", `Bearer ${token}`);
  //        cookie 이름, Bearer 토큰 형식으로 token 정보 전달
  return res.status(200).json({ message: "로그인에 성공하였습니다" });
});

 

Insomnia에서 로그인을 시도해보면,

 

 

암호화된 비밀번호를 가진 email이 "clientUser@naver.com"인

유저가 정상적으로 로그인 된 것을 확인할 수 있습니다.

 

만약, 비밀번호를 틀릴 경우

 

"비밀번호가 일치하지 않습니다"라는 문구가 Client에게 응답됩니다.

 

그리고, 이메일이 존재하지 않을 경우,

"존재하지 않는 이메일입니다"라는 문구가 Client에게 응답됩니다.

 

사용자 인증 미들웨어 만들기

사용자 인증 미들웨어는 클라이언트로 부터 전달받은 쿠키를 검증하는 작업을 수행합니다.

클라이언트가 제공한 쿠키에 담겨있는 JWT를 이용해 사용자를 조회하도록 구현할 예정입니다.

 

[게시판 프로젝트] 사용자 인증 미들웨어 비즈니스 로직

  1. 클라이언트로 부터 **쿠키(Cookie)**를 전달받습니다.
  2. **쿠키(Cookie)**가 Bearer 토큰 형식인지 확인합니다.
  3. 서버에서 발급한 JWT가 맞는지 검증합니다.
  4. JWT의 userId를 이용해 사용자를 조회합니다.
  5. req.user 에 조회된 사용자 정보를 할당합니다.
  6. 다음 미들웨어를 실행합니다.

사용자 인증 미들웨어는 클라이언트가 전달한 쿠키를 바탕으로 사용자를 검증합니다.

이 과정에서 토큰이 만료되지 않았는지, 토큰 형식은 일치하는지,

서버가 발급한 토큰이 맞는지 등 다양한 검증을 수행하여,

사용자의 권한을 확인합니다.

 

이렇게, JWT를 통한 사용자를 인증하는 것을 인증(Authentication)이라고 부릅니다.

 

middlewares 폴더를 생성하고, auth.middleware.js 파일을 생성합니다.

 

import jwt from "jsonwebtoken";
import prisma from "../src/utils/prisma/index.js";

// 1. 클라이언트로 부터 **쿠키(Cookie)**를 전달받습니다.
// 2. **쿠키(Cookie)**가 **Bearer 토큰** 형식인지 확인합니다.
// 3. 서버에서 발급한 **JWT가 맞는지 검증**합니다.
// 4. JWT의 `userId`를 이용해 사용자를 조회합니다.
// 5. `req.user` 에 조회된 사용자 정보를 할당합니다.
// 6. 다음 미들웨어를 실행합니다.

export default async function (req, res, next) {
  try {
    const { authorization } = req.cookies;
    if (!authorization) throw new Error("토큰이 존재하지 않습니다.");

    // 현재 토큰이 "Bearer ~~~~~"이기 때문에
    // split한 배열의 0번지는 Bearer이고, 1번은 "암호화에 사용한 키" 이다.
    const [tokenType, token] = authorization.split(" ");

    if (tokenType !== "Bearer")
      throw new Error("토큰 타입이 일치하지 않습니다.");

    const decodedToken = jwt.verify(token, "custom-secret-key");
    const userId = decodedToken.userId;

    const user = await prisma.users.findFirst({
      where: { userId: +userId },
    });
    if (!user) {
      res.clearCookie("authorization");
      throw new Error("토큰 사용자가 존재하지 않습니다.");
    }

    // req.user의 정보를 user 정보로 제공해준다.
    req.user = user;

    // 다음 미들웨어가 호출될 수 있도록 next()를 사용한다.
    next();
  } catch (error) {
    res.clearCookie("authorization");

    // 토큰이 만료되었거나, 조작되었을 때, 에러 메시지를 다르게 출력합니다.
    switch (error.name) {
      case "TokenExpiredError":
        return res.status(401).json({ message: "토큰이 만료되었습니다." });
      case "JsonWebTokenError":
        return res.status(401).json({ message: "토큰이 조작되었습니다." });
      default:
        return res
          .status(401)
          .json({ message: error.message ?? "비정상적인 요청입니다." });
    }
  }
}

 

사용자 인증 미들웨어 만들기를 바탕으로 다른 사용자의 정보를 조회할 수 있게 만들 수 있습니다.

(우린 본인의 사용자 정보를 출력시켜볼 계획입니다.)

 

이것을 사용자 정보 조회 API라고 만들어봅시다.

 

사용자 정보 조회 API

게시판 프로젝트] 사용자 정보 조회 API 비즈니스 로직

  1. 클라이언트가 로그인된 사용자인지 검증합니다.
  2. 사용자를 조회할 때, 1:1 관계를 맺고 있는 UsersUserInfos 테이블을 조회합니다.
  3. 조회한 사용자의 상세한 정보를 클라이언트에게 반환합니다.

사용자 정보 조회 API는 Users 테이블 뿐만 아니라 UserInfo 테이블도 함께 조회한다.

테이블을 각각 1번 씩 조회를 하게 돼 총 2번의 조회를 하게 되는 문제가 발생합니다.

 

이런 문제를 해결하기 위해 Prisma에서는 중첩 Select문법제공합니다.

 

Insomnia에서 확인하기 위해 먼저,

로그인을 진행해주고,

 

localhost:3018/api/users

 

로 사용자 정보 조회를 수행 합니다.

 

이때, 

 

Manage Cookies를 확인하면,

해당 "clientUser@naver.com"이라는 유저의 쿠키 정보를 확인할 수 있고,

 

조회를 누르면 본인의 사용자 정보를 확인할 수 있습니다.

 

추가 설명

 

Prisma에서 연관 관계 테이블을 어떻게 조회하나요?

 

Select 내에 또 다른 Select가 존재하는데, 이것을 중첩 Select문법이라고 부릅니다.

중첩 Select는 SQL의 JOIN과 동일한 역할을 수행합니다.

 

만약, 현재 테이블과 연관된 테이블의 모든 컬럼을 조회하고 싶다면,

include 문법으로도 조회할 수 있습니다.