X

현서·2025년 4월 28일

백엔드

목록 보기
8/18
post-thumbnail

X (구 트위터)

JWT, bcrypt를 저번에 만들어 둔 X에 입혀보자.
git의 새로운 가지를 생성하여 일단 그곳에 저장해두었다.
다음은 커맨드 창에서 실행한 결과이다.

git branch
  main
* session-auth // 원래는 이 가지였는데!

C:\Hsgo\X>git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

C:\Hsgo\X>git branch
* main // 저장소를 main으로 옮겼다!
  session-auth

C:\Hsgo\X>git checkout -b jwt-auth
Switched to a new branch 'jwt-auth'

C:\Hsgo\X>git branch
* jwt-auth // 새로운 가지 생성! 새로운 저장소 생성!
  main
  session-auth

X에는 express-validator가 없어서 이것도 설치해주고...

npm i express-validator

민감한 정보를 보호하기 위해 .env 파일을 .gitignore에 추가하여 Git과 같은 버전 관리 시스템에 커밋되지 않도록 한다.

JWT_SECRET이나 데이터베이스 비밀번호와 같은 중요한 정보는 소스 코드에 하드코딩해서 저장하면 안 된다.
이 정보를 .env 파일에 저장함으로써 코드와 분리하고, 이를 외부에 노출하지 않도록 한다.

.env

JWT_SECRET=abcdefg1234%^&*
JWT_EXPIRES_SEC=172800
BCRYPT_SALT_ROUNDS=10
HOST_PORT=9090

.gitignore

# DB
.env

# node.js
node_modules/
package-lock.json

# MACOS
.DS_Store

설치

npm i dotenv

➿ config.mjs

import dotenv from "dotenv";

dotenv.config();

function required(key, defaultValue = undefined) {
  const value = process.env[key] || defaultValue;
  if (value == null) {
    throw new Error(`키 ${key}는 undefined!!`);
  }
  return value;
}

export const config = {
  jwt: {
    secretKey: required("JWT_SECRET"),
    expiresInSec: parseInt(required("JWT_EXPIRES_SEC", 86400)), // 기본은 86400초(하루)
  },
  bcrypt: {
    saltRounds: parseInt(required("BCRYPT_SALT_ROUNDS", 10)), // 기본은 10
  },
  host: {
    port: parseInt(required("HOST_PORT", 9090)),
  },
};

✨ app.mjs

import express from "express";
import postsRouter from "./router/posts.mjs";
import authRouter from "./router/auth.mjs";
import { config } from "./config.mjs";
import cors from "cors"; // 추가

const app = express();
app.use(cors()); // 추가

app.use(express.json());

app.use("/posts", postsRouter);
app.use("/auth", authRouter);

app.use((req, res, next) => {
  res.sendStatus(404);
});

app.listen(config.host.port);


파일 경로는 이렇게 되어 있다.

data 폴더 안에 auth.mjs

let users = [
  {
    id: "1",
    userid: "apple",
    password: "1111",
    name: "김사과",
    email: "apple@apple.com",
    url: "https://randomuser.me/api/portraits/women/32.jpg",
  },
  {
    id: "2",
    userid: "banana",
    password: "2222",
    name: "반하나",
    email: "banana@banana.com",
    url: "https://randomuser.me/api/portraits/women/44.jpg",
  },
  {
    id: "3",
    userid: "orange",
    password: "3333",
    name: "오렌지",
    email: "orange@orange.com",
    url: "https://randomuser.me/api/portraits/men/11.jpg",
  },
  {
    id: "4",
    userid: "berry",
    password: "4444",
    name: "배애리",
    email: "orange@orange.com",
    url: "https://randomuser.me/api/portraits/women/52.jpg",
  },
  {
    id: "5",
    userid: "melon",
    password: "5555",
    name: "이메론",
    email: "orange@orange.com",
    url: "https://randomuser.me/api/portraits/men/29.jpg",
  },
];

// 회원가입 사용자 추가
export async function createUser(userid, password, name, email) {
  const user = {
    id: Date.now().toString(),
    userid,
    password,
    name,
    email,
    url: "https://randomuser.me/api/portraits/men/29.jpg",
  };
  users = [user, ...users];
  return users;
}

// 로그인
export async function login(userid, password) {
  const user = users.find(
    (user) => user.userid === userid && user.password === password
  );
  return user;
}

export async function findByUserid(userid) {
  return users.find((user) => user.userid === userid);
}

export async function findByid(id) {
  return users.find((user) => user.id === id);
}

data 폴더 안에 post.mjs

let posts = [
  {
    id: "1",
    name: "김사과",
    userid: "apple",
    text: "Node.js 배우는 중인데 Express 진짜 편하다! :로켓:",
    createdAt: Date.now().toString(),
    url: "https://randomuser.me/api/portraits/women/32.jpg",
  },
  {
    id: "2",
    name: "반하나",
    userid: "banana",
    text: "오늘의 커피 :커피:️ + 코딩 = 최고의 조합!",
    createdAt: Date.now().toString(),
    url: "https://randomuser.me/api/portraits/women/44.jpg",
  },
  {
    id: "3",
    name: "오렌지",
    userid: "orange",
    text: "Elasticsearch 연동 완료! 실시간 검색 API 짜릿해 :돋보기:",
    createdAt: Date.now().toString(),
    url: "https://randomuser.me/api/portraits/men/11.jpg",
  },
  {
    id: "4",
    name: "배애리",
    userid: "berry",
    text: "JavaScript 비동기 너무 어렵다... Promises, async/await, 뭐가 뭔지 :울음:",
    createdAt: Date.now().toString(),
    url: "https://randomuser.me/api/portraits/women/52.jpg",
  },
  {
    id: "5",
    name: "이메론",
    userid: "melon",
    text: "새 프로젝트 시작! Express + MongoDB + EJS 조합 좋아요 :전구:",
    createdAt: Date.now().toString(),
    url: "https://randomuser.me/api/portraits/men/29.jpg",
  },
];

// 모든 포스트를 리턴
export async function getAll() {
  return posts;
}

// 사용자 아이디(userid)에 대한 포스트를 리턴
// 조건을 만족하는 모든 요소를 배열로 리턴
export async function getAllByUserid(userid) {
  return posts.filter((post) => post.userid === userid);
}

// 글 번호(id)에 대한 포스트를 리턴
// 조건을 만족하는 첫 번째 요소 하나를 리턴
export async function getById(id) {
  return posts.find((post) => post.id === id);
}

// 포스트 작성
export async function create(userid, name, text) {
  const post = {
    id: Date.now().toString(),
    userid,
    name,
    text,
    createdAt: Date.now().toString(),
  };
  posts = [post, ...posts];
  return posts;
}

// 포스트 변경
export async function update(id, text) {
  const post = posts.find((post) => post.id === id);
  if (post) {
    post.text = text;
  }
  return post;
}

// 포스트 삭제
export async function remove(id) {
  posts = posts.filter((post) => post.id !== id);
}

controller 폴더 안에 auth.mjs

import * as authRepository from "../data/auth.mjs";
import * as bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { config } from "../config.mjs";

const secretKey = config.jwt.secretKey;
const bcryptSaltRounds = config.bcrypt.saltRounds;
const jwtExpiresInDays = config.jwt.expiresInSec;

async function createJwtToken(id) {
  return jwt.sign({ id }, secretKey, { expiresIn: jwtExpiresInDays });
}

// 회원가입을 진행하는 함수
export async function signup(req, res, next) {
  const { userid, password, name, email } = req.body;

  // 회원 중복 체크
  const found = await authRepository.findByUserid(userid);
  if (found) {
    return res.status(409).json({ message: `${userid}이 이미 있습니다.` });
  }

  const hashed = bcrypt.hashSync(password, bcryptSaltRounds);
  const users = await authRepository.createUser(userid, hashed, name, email);
  const token = await createJwtToken(users.id);
  console.log(token);

  if (users) {
    res.status(201).json({ token, userid });
  }
}

// 로그인
export async function login(req, res, next) {
  const { userid, password } = req.body;
  const user = await authRepository.findByUserid(userid);
  if (!user) {
    res.status(401).json(`${userid} 아이디를 찾을 수 없음`);
  }
  const isValidPassword = await bcrypt.compare(password, user.password);
  if (!isValidPassword) {
    return res.status(401).json({ message: "아이디 또는 비밀번호 확인" });
  }

  const token = await createJwtToken(user.id);
  res.status(200).json({ token, userid });
}

export async function verify(req, res, next) {
  const id = req.id;
  if (id) {
    res.status(200).json(id);
  } else {
    res.status(401).json({ message: "사용자 인증 실패" });
  }
}

export async function me(req, res, next) {
  const user = await authRepository.findByid(req.id);
  if (!user) {
    return res.status(404).json({ message: "일치하는 사용자가 없음" });
  }
  res.status(200).json({ token: req.token, userid: user.userid });
}

controller 폴더 안에 post.mjs

import * as postRepository from "../data/post.mjs";

// 모든 포스트 / 해당 아이디에 대한 포스트를 가져오는 함수
export async function getPosts(req, res, next) {
  const userid = req.query.userid;
  const data = await (userid
    ? postRepository.getAllByUserid(userid)
    : postRepository.getAll());
  res.status(200).json(data);
}

// id를 받아 하나의 포스트를 가져오는 함수
export async function getPost(req, res, next) {
  const id = req.params.id;
  const post = await postRepository.getById(id);
  if (post) {
    res.status(200).json(post);
  } else {
    res.status(404).json({ message: `${id}의 포스트가 없습니다.` });
  }
}

// 포스트를 생성하는 함수
export async function createPost(req, res, next) {
  const { userid, name, text } = req.body;
  const posts = await postRepository.create(userid, name, text);
  res.status(201).json(posts);
}

// 포스트를 변경하는 함수
export async function updatePost(req, res, next) {
  const id = req.params.id;
  const text = req.body.text;
  const post = await postRepository.update(id, text);
  if (post) {
    res.status(201).json(post);
  } else {
    res.status(404).json({ message: `${id}의 포스트가 없습니다.` });
  }
}

// 포스트를 삭제하는 함수
export async function deletePost(req, res, next) {
  const id = req.params.id;
  await postRepository.remove(id);
  res.sendStatus(204);
}

middleware 폴더 안에 있는 auth.mjs

/*
    Authorization
    - 본인의 신원을 증명하는 과정
    
    Authorization 헤더
    - http 요청을 보낼 때 헤더(Headers)라는 곳에 "추가정보"를 담을 수 있음
    - 인증정보를 담는 표준 위치가 Authorization 헤더임

    Bearer
    - Authorization에 실을 수 있는 방식(타입) 중 하나
    - Bearer 토큰(token)을 가지고 있다는 것 자체로 인증함
        Authorization: Bearer <토큰>
*/
import jwt from "jsonwebtoken";
import * as authRepository from "../data/auth.mjs";
import { config } from "../config.mjs";

const AUTH_ERROR = { message: "인증에러" };

export const isAuth = async (req, res, next) => {
  const authHeader = req.get("Authorization");
  console.log(authHeader);

  if (!(authHeader && authHeader.startsWith("Bearer "))) {
    console.log("헤더 에러");
    return res.status(401).json(AUTH_ERROR);
  }
  // Bearer dafkjdkfj;lkdja;dkj;lkdj;k
  const token = authHeader.split(" ")[1];
  console.log(token);

  jwt.verify(token, config.jwt.secretKey, async (error, decoded) => {
    if (error) {
      console.log("토큰 에러");
      return res.status(401).json(AUTH_ERROR);
    }
    console.log(decoded.id);
    const user = await authRepository.findByid(decoded.id);
    if (!user) {
      console.log("아이디 없음");
      return res.status(401).json(AUTH_ERROR);
    }
    console.log("user.id: ", user.id);
    console.log("user.userid: ", user.userid);
    req.userid = user.userid;
    next();
  });
};

middleware 폴더 안에 있는 validator.mjs

import { validationResult } from "express-validator";

export const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (errors.isEmpty()) {
    return next();
  }
  return res.status(400).json({ message: errors.array()[0].msg });
};

router 폴더 안에 있는 auth.mjs

import express from "express";
import * as authController from "../controller/auth.mjs";
import { body } from "express-validator";
import { validate } from "../middleware/validator.mjs";

const router = express.Router();

const validateLogin = [
  body("userid")
    .trim()
    .isLength({ min: 4 })
    .withMessage("최소 4자 이상 입력")
    .matches(/^[a-zA-Z0-9]*$/)
    .withMessage("특수문자는 사용불가"),
  body("password")
    .trim()
    .isLength({ min: 8 })
    .withMessage("최소 8자 이상 입력"),
  validate,
];

const validateSignup = [
  ...validateLogin,
  body("name").trim().notEmpty().withMessage("name을 입력"),
  body("email").trim().isEmail().withMessage("이메일 형식 확인"),
  validate,
];

// 회원가입
// POST
// http://127.0.0.1:9090/auth/signup
router.post("/signup", validateSignup, authController.signup);

// 로그인
// POST
// http://127.0.0.1:9090/auth/login
router.post("/login", validateLogin, authController.login);

export default router;

router 폴더 안에 있는 posts.mjs

import express from "express";
import * as postController from "../controller/post.mjs";
import { body } from "express-validator";
import { validate } from "../middleware/validator.mjs";
import { isAuth } from "../middleware/auth.mjs";

const router = express.Router();

const validatePost = [
  body("text").trim().isLength({ min: 5 }).withMessage("최소 5자 이상 입력"),
  validate,
];

// 모든 포스트 가져오기
// 해당 아이디에 대한 포스트 가져오기
// GET
// http://127.0.0.1:9090/posts/
// http://127.0.0.1:9090/posts?userid=apple
router.get("/", isAuth, postController.getPosts);

// 글번호에 대한 포스트 가져오기
// GET
// http://127.0.0.1:9090/posts/:id
router.get("/:id", isAuth, postController.getPost);

// 포스트 쓰기
// POST
// http://127.0.0.1:9090/posts
// json 형태로 입력 후 저장
router.post("/", validatePost, isAuth, postController.createPost);

// 포스트 수정하기
// PUT
// http://127.0.0.1:9090/posts/:id
// json 형태로 입력 후 저장
router.put("/:id", validatePost, isAuth, postController.updatePost);

// 포스트 삭제하기
// DELETE
// http://127.0.0.1:9090/posts/:id
router.delete("/:id", isAuth, postController.deletePost);

export default router;

Postman에서 결과를 확인해본다.

왼쪽 메뉴에서 Collections에 create new collection하여 폴더를 만들어두고, 저장해 둘 수 있다.

회원가입하고, 로그인 한 후
로그인 Token을 Token 칸에 입력한 후 저장한다.
그래야 포스트를 가져오고, 쓰고, 수정하고, 저장하고, 삭제할 수 있다.

깃허브에서 모든 가지 병합 후,
main으로 돌아오면, 다른 가지에서 작업했던 미들웨어(middleware) 폴더가 사라져있다.
풀(PULL)하면 병합 상태로 돌아온다.

profile
The light shines in the darkness.

0개의 댓글