[프로그래머스] Node기반 REST API 구현(3)

Lina Hongbi Ko·2024년 10월 2일
0

Programmers_BootCamp

목록 보기
28/76
post-thumbnail

2024년 10월 2일

✏️ users

📍 db랑 연결시키기

// mariadb.js

// mysql 모듈 소환
const mariadb = require("mysql2");

// DB와 연결 통로 생성
const connection = mariadb.createConnection({
  host: "127.0.0.1",
  user: "root",
  password: "root",
  database: "Bookshop",
   dateStrings: true
});

module.exports = connection;

📍 users 작업 전

  • 회원가입시 email, password만 받아오기 때문에 username은 삭제해준다

  • users.js를 db에 연결시켜주고 api를 본격적으로 넣어 사용해보자

📍 회원가입

// users.js

const express = require("express");
const router = express.Router();
const conn = require('../mariadb');
router.use(express.json());

// 회원가입
router.post("/join", (req, res) => {
  const {email, password} = req.body;

  let sql = 'INSERT INTO users (email, password) VALUES (?, ?)';
  let values = [email, password];
  conn.query(sql, values, (err, results) => {
    if(err) {
      console.log(err);
      return res.status(400).end(); // BAD REQUEST
    }
    
    res.status(201).json(results);

  });
});

🔍 http-status-codes 모듈

  • status code를 숫자로 써주면 무슨 내용인지 모를 수 있고, 오타가 날 수 있음
  • 그래서, http-status-codes 모듈 활용해보자
  • npm install http-status-codes —save
  • 그리고 모듈 불러오기
// users.js

const express = require("express"); // express 모듈
const router = express.Router();
const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
router.use(express.json());

// 회원가입
router.post("/join", (req, res) => {
  const { email, password } = req.body;

  let sql = "INSERT INTO users (email, password) VALUES (?, ?)";
  let values = [email, password];
  conn.query(sql, values, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }
    return res.status(StatusCodes.CREATED).json(results);
  });
});

... 생략 ...

POSTMAN) POST + localhost:9999/users/join + {”email” : “kim@mail.com”, “password” : 1111}

🔍 node.js 패키지 구조(feat.컨트롤러)

  • node.js 패키지 구조를 보면,

    • app.js : 프로젝트의 메인 라우터 역할
      • routes / users.js, books.js … : 하위 라우터 역할 ⇒ 경로를 찾아줌
  • 그런데, 라우터는 경로를 찾아주는 역할만 하는데 우리는 로직까지 다 짜주고 있다

  • 한곳에 코드를(로직을) 다 작성하면 나중에 수정하기 어려움

  • 라우터가 로직까지 다 수행할 때 단점
    - 프로젝트 규모가 커질수록, 코드가 복잡해짐
    - 가독성 떨어짐
    - 트러블 슈팅(에러를 찾아서 해결하는 작업) 어려움
    ⇒ “유지 보수” 하기 어려움
    *유지보수 : 프로그램을 계속 운영하면서 요구사항을 반영하고 에러를 해결하는 것을 말함
    ⇒ 해결 방법 : 코드를 간결하고, 가독성이 높게 만들어주는 것!!

  • 어쨌든, 그래서 코드를 빼내야함 -> 콜백함수를 빼내야함!!
    - 콜백함수 ⇒ 경로를 찾은 다음 역할

  • 컨트롤러 : 프로젝트에서 매니저 역할을 하는 파일 : 관장
    - 누군가에게 일을 어떻게 시켜야할지 알고 있음(= 직접 일을 하진 않을 것)
    - 라우터를 통해서 “사용자의 요청(request)이” 길(url)을 찾아오면 매니저(콜백함수 = controller)가 환영해 줄 것임
    - 그리고 매니저는 직접 일을 하진 않고, 알바생(service)한테 일을 시키고, 알바생이 일을 한다음 결과물을 매니저에게 전달
    - 그다음, 매니저(controller)는 사용자에게 response를 돌려준다.

  • 라우터는 길을 찾는 용도로만 사용!

  • 컨트롤러를 만들어보자

// controller / UserController.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈

const join = (req, res) => {
  const { email, password } = req.body;

  let sql = "INSERT INTO users (email, password) VALUES (?, ?)";
  let values = [email, password];
  conn.query(sql, values, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }

    return res.status(StatusCodes.CREATED).json(results);
  });
};

module.exports = join;
const express = require("express"); // express 모듈
const router = express.Router();
const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
router.use(express.json());

const join = require("../controller/UserController");

// 회원가입
router.post("/join", join);

... 생략 ...

POSTMAN) POST + localhost:9999/users/join + {”email” : “lee@email.com”, “password” : “2222”}
POST + localhost:9999/users/join + {”email” : “park@email.com”, “password” : “3333”}

📍 로그인

  • users 컨트롤러 모듈화 정리
// UserController.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈

const join = (req, res) => {
  const { email, password } = req.body;

  let sql = "INSERT INTO users (email, password) VALUES (?, ?)";
  let values = [email, password];
  conn.query(sql, values, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }

    return res.status(StatusCodes.CREATED).json(results);
  });
};

const login = (req, res) => {
  res.json("로그인");
};

const passwordResetRequest = (req, res) => {
  res.json("비밀번호 초기화 요청");
};

const passwordReset = (req, res) => {
  res.json("비밀번호 초기화");
};

module.exports = { join, login, passwordResetRequest, passwordReset };
// users.js

const express = require("express"); // express 모듈
const router = express.Router();
const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
router.use(express.json());

const {
  join,
  login,
  passwordResetRequest,
  passwordReset,
} = require("../controller/UserController");

router.post("/join", join); // 회원가입
router.post("/login", login); // 로그인
router.post("/reset", passwordResetRequest); // 비밀번호 초기화 요청
router.put("/reset", passwordReset); // 비밀번호 초기화

module.exports = router;
  • 로그인 구현
    • 우리는 response를 쿠키로 보낼 예정이므로 api설계를 수정한다.

// UserController.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();

const login = (req, res) => {
  const { email, password } = req.body;

  let sql = `SELECT * FROM users WHERE email = ?`;
  conn.query(sql, email, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }
    const loginUser = results[0];
    if (loginUser && loginUser.password == password) {
      // 토큰 발행
      const token = jwt.sign(
        {
          email: loginUser.email,
        },
        process.env.PRIVATE_KEY,
        {
          expiresIn: "5m",
          issuer: "hongbi",
        }
      );
      // 토큰 쿠키에 담기
      res.cookie("token", token, { httpOnly: true });
      console.log(token);

      res.status(StatusCodes.OK).json(results);
    } else {
      res.status(StatusCodes.UNAUTHORIZED).end();
      // 403 : Forbidden (접근 권리 없음)
      // 401 : Unauthorized (미인증 상태)
      // 403은 접근 권리가 없다는 뜻인데, 서버는 그 사람이 누구인지 알고 있지만
      // 401은 그 사람이 누군지 모름
    }
  });
};

... 생략 ...

POSTMAN) POST + localhost:9999/users/login + {”email” : “kim@email.com”, “password” : “1111”}

📍 비밀번호 초기화 요청

  • 요청하면 이메일을 다음 페이지에서 받아야 하므로 response에 email를 담는다

// UserController.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();

const passwordResetRequest = (req, res) => {
  const { email } = req.body;

  let sql = `SELECT * FROM users WHERE email = ?`;
  conn.query(sql, email, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }

    const user = results[0];
    if (user) {
      return res.status(StatusCodes.OK).json({
        email: email,
      });
    } else {
      return res.status(StatusCodes.UNAUTHORIZED).end();
    }
  });
};

📍 비밀번호 초기화

  • 비밀번호 초기화 요청을 하면 웹브라우저(이전 페이지에서)에서 알고 있는 이메일을 가지고 들어와야함
  • 프론트에서 이메일을 확인해서 이메일이 없을 순 없지만, 만약을 대비한 예외 처리도 해야함
if(results.affectedRows == 0) {
    return res.status(StatusCodes.BAD_REQUEST).end();
} else {
    return res.status(StatusCodes.OK).json(results);
}
// UserController.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();

const passwordReset = (req, res) => {
  const { email, password } = req.body;

  let sql = `UPDATE users SET password = ? WHERE email = ?`;
  let values = [password, email];
  conn.query(sql, values, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }
    if (results.affectedRows == 0) {
      return res.status(StatusCodes.BAD_REQUEST).end();
    } else {
      return res.status(StatusCodes.OK).json(results);
    }
  });
};

비밀번호 초기화 요청 : POSTMAN) POST + localhost:9999/users/reset + {”email” : “kim@email.com”}

비밀번호 초기화 : PUT + localhost:9999/users/reset + {”email” : “kim@email.com”, “password” : “1010”}

📍 회원가입시 비밀번호 암호화

  • 비밀번호가 날 것으로 적혀 있어서 비밀번호가 털리면 큰일남 → 암호화 해야함
  • 모듈을 사용해서 암호화할 예정
  • Node.js의 내장 모듈인 crypto를 사용해보자

crypto : node.js에서 제공하는 기본 내장모듈로서 암호화에 사용이 된다.

// UserController.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const crypto = require("crypto"); // crypto 모듈 : 암호화
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();
  • 회원가입 할 때 암호화해서 넣었어야 했는데 놓쳤으므로, 4번째 친구 가입할 때 password 암호화 시켜보자.
  • 지금부터 로직 바꾸기 시작.
// UserController.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const crypto = require("crypto"); // crypto 모듈 : 암호화
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();

const join = (req, res) => {
  const { email, password } = req.body;

  // 비밀번호 암호화
  const salt = crypto.randomBytes(64).toString("base64");
  const hashPassword = crypto
    .pbkdf2Sync(password, salt, 10000, 64, "sha512")
    .toString("base64");

  let sql = "INSERT INTO users (email, password) VALUES (?, ?)";
  let values = [email, password];
  conn.query(sql, values, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }

    return res.status(StatusCodes.CREATED).json(results);
  });
};
  • salt : hashPassword를 만들어주기 위한것(= 비밀번호를 암호화하는데 랜덤한 값을 넣어 늘 새로운 암호화 비밀번호를 만들 수 있게 도와주는 난수)

    • randomBytes(64) : 매개변수로 들어오는 숫자를 가지고 randombyte를 만듦 → 64만큼의 길이로 바이트 값을 랜덤으로 만들어줌
    • toString(64) : 만든 다음 base64방식으로 문자열 만듦(아스키코드로 인코딩)
    • 해석 : 길이가 64인 무작위 salt(소금)를 생성하고, 이를 Base64로 인코딩하여 문자열로 변환한다는 의미
  • hashPassword

    • pdkdf25ync() : 본격적으로 hashPassword를생성
      • password: 해싱할 값, 사용자의 비밀번호
      • salt : 랜덤하게 들고 왔던 값(앞서 생성한 소금)
      • 10000 : 해시함수를 반복하는 횟수(이 값이 클수록 보안이 강화)
      • 64 : 몇자리로 만들지(생성되는 해시의 길이_바이트 단위)
      • Sah512 : 해시 알고리즘 (여기서는 SHA-512를 사용)
  • 테스트해보자

// demo-node / crypto-demo.js

const crypto = require('crypto');

const password = "1111";

const salt = crypto.randomBytes(64).toString("base64");
  const hashPassword = crypto
    .pbkdf2Sync(password, salt, 10000, 10, "sha512")
    .toString("base64");
    
console.log(hashPassword);

서버 재작동 하면, 비밀번호 다시 달라짐

  • 그러나!!!이 알고리즘은 단방향임

    • 암호화가 된다면 복호화 되지 않음
    • crypto는 복호화(Decoding)이 안됨
  • 그럼 어떻게 로그인 해야할까?

    • 게다가 비밀번호는 계속 바뀌는데??
      • 바뀌는 이유는 salt를 randombyte을 가져와서 계속 바꾸기 때문
        • salt를 고정(1)하거나 salt가 매번 바뀌므로 데이터베이스에 넣어서 사용(2)
          1. 회원가입 할때, 암호화해서 암호화된 비밀번호와 salt값을 같이 저장한다.
          2. 로그인시, 이메일 & 비밀번호 (날 것) ⇒ salt값 꺼내서 비밀번호 암호화 해보고 ⇒ 디비 비밀번호랑 비교
    • salt를 저장해보자
      • 워크벤치 켜서 users테이블에 salt 필드 추가(컬럼 생성)
  • 이제 salt를 insert해보자

// Usercontroller.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const crypto = require("crypto"); // crypto 모듈 : 암호화
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();

const join = (req, res) => {
  const { email, password } = req.body;

  let sql = "INSERT INTO users (email, password, salt) VALUES (?, ?, ?)";

  // 회원 가입시 비밀번호를 암호화해서 암호화된 비밀번호와 salt 값을 같이 저장
  const salt = crypto.randomBytes(10).toString("base64");
  const hashPassword = crypto
    .pbkdf2Sync(password, salt, 10000, 10, "sha512")
    .toString("base64");

  let values = [email, hashPassword, salt];

  // 로그인 시, 이메일 & 비밀번호(날 것) => salt값 꺼내서 비밀번호 암호화 해보고 => 디비 비밀번호랑 비교
  conn.query(sql, values, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }

    return res.status(StatusCodes.CREATED).json(results);
  });
};

POSTMAN) POST + localhost:9999/users/join + {”email” : “choi@mail.com”, “password” : “4444”}

📍 암호화한 비밀번호 로그인

  • 그럼 우리는 암호화한 비밀번호로 어떻게 로그인할까?
    • salt값을 이용할 것임
      -> 로그인시 입력한 password를 users테이블의 salt값으로 저장한 애로 암호화 시킴
      const hashPassword = crypto
          .pbkdf2Sync(password, loginUser.salt, 10000, 10, "sha512")
          .toString("base64");
  • 따라서, 로그인을 할 때 입력한 password는 DB에 저장된 salt값으로 hashpassword가 되어서, 같이 저장 했던 데이터베이스의 암호화된 hashpassword와 같은지 비교하는 것임
// Usercontroller.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const crypto = require("crypto"); // crypto 모듈 : 암호화
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();


const login = (req, res) => {
  const { email, password } = req.body;

  let sql = `SELECT * FROM users WHERE email = ?`;

  conn.query(sql, email, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }

    const loginUser = results[0];

    // salt값 꺼내서 날 것으로 들어온 비밀번호를 암호화 해보고
    const hashPassword = crypto
      .pbkdf2Sync(password, loginUser.salt, 10000, 10, "sha512")
      .toString("base64");

    // => 디비 비밀번호랑 비교
    if (loginUser && loginUser.password == hashPassword) {
      // 토큰 발행
      const token = jwt.sign(
        {
          email: loginUser.email,
        },
        process.env.PRIVATE_KEY,
        {
          expiresIn: "5m",
          issuer: "hongbi",
        }
      );
      // 토큰 쿠키에 담기
      res.cookie("token", token, { httpOnly: true });
      console.log(token);

      res.status(StatusCodes.OK).json(results);
    } else {
      res.status(StatusCodes.UNAUTHORIZED).end();
    }
  });
};

POSTMAN) POST + localhost:9999/users/login + {”choi@email.com”, “password” : “4444”}

  • 비밀 번호 초기화도 암호화 시켜야함
// UserControoler.js

const conn = require("../mariadb"); // db 모듈
const { StatusCodes } = require("http-status-codes"); // status code 모듈
const jwt = require("jsonwebtoken"); // jwt 모듈
const crypto = require("crypto"); // crypto 모듈 : 암호화
const dotenv = require("dotenv"); // dotenv 모듈
dotenv.config();

const passwordReset = (req, res) => {
  const { email, password } = req.body;

  let sql = `UPDATE users SET password = ?, salt = ? WHERE email = ?`;

  // 암호화된 비밀번호와 salt 값을 같이 DB에 저장
  const salt = crypto.randomBytes(10).toString("base64");
  const hashPassword = crypto
    .pbkdf2Sync(password, salt, 10000, 10, "sha512")
    .toString("base64");

  let values = [hashPassword, salt, email];
  conn.query(sql, values, (err, results) => {
    if (err) {
      console.log(err);
      return res.status(StatusCodes.BAD_REQUEST).end();
    }
    if (results.affectedRows == 0) {
      return res.status(StatusCodes.BAD_REQUEST).end();
    } else {
      return res.status(StatusCodes.OK).json(results);
    }
  });
};

POSTMAN) PUT + localhost:9999/users/reset + {”email” : “kim@email.com”, “password” : “1111”}

  • 로그인 해보면, 잘 들어가는 것 확인

🍏🍎 오늘의 느낀점 : 와우,, 오늘 실습은 정말 흥미로웠다. 비밀번호를 암호화하는 새로운 방법을 배웠는데 모듈을 통해 비교적 쉽게(?) 할 수 있는 방법을 알아서 나중에 꼭 써먹어봐야겠다. 그리고 statuscode도 모듈을 통해 프론트단에게 전달해주려고 문자열로 연결해주는 방법이 있는 것을 알았고, 역시 오류를 없애려고 애써야 겠구나 하는 생각도 들었다. 그리고 로직들을 짜면서 api 설계가 바뀌면서 요청, 응답을 어떻게 해줘야 더 좋은 로직이 될지 많은 고민을 해보야겠구나 하는 생각이 들었다.

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글