프로젝트 3-1 ~ 3-7 (비밀번호 암호화)

Develop Kim·2024년 10월 2일

programmers

목록 보기
22/40

프로젝트: Node.js 기반의 REST API 구현

1 설계를 직접 해보자

1-1 ~ 1-6(API 설계)

1-7 ~ 1-11(ERD 설계)

2 프로젝트 구현 시작

1-12 ~ 2-5(API 설계 점검, 기본 세팅(모듈화, postman, diagram, workbranch))

3 회원 API(feat. 비밀번호 암호화)

3-1 회원가입 API 구현

1️⃣ 회원가입 API를 구현해보자

  • 먼저, mariadb.js를 추가하고
// mysql 모듈 소환
const mariadb = require('mysql2');

//db와 연결통로 생성
const connection = mariadb.createConnection({
  host : 'localhost',
  user : 'root',
  password : 'rood',
  database : 'Bookshop',
  dataString : true
});

module.exports = connection;
  • users.js에서 회원가입 API를 만들어준다.
const express = require("express");
const router = express.Router();
const conn = require('../mariadb');

router.use(express.json()); // POST를 사용하면 값을 json형태로 받아오기 때문에 추가

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

  let sql = 'INSERT INTO users (email, password) VALUES (?, ?)'
  let values = [eamil, password]

  conn.query(sql, values,
    (err, results) => {
      if(err) {
        console.log(err);
        return res.status(400).end // BAD REQUEST
      }
      res.status(201).json(results)
    })
  res.json('회원가입');
});



3-2 http-status-codes 모듈 활용해보기

1️⃣ status를 더 정확하게 표현해보자

  • 200, 400으로 status를 프론트에 넘겨주는 것 보다 더 정확히 표현하는 방법이 있다.
  • http-status-codes를 이용하면 아래처럼 문자열로 상태를 넘겨줄 수 있다.

  • users.js에 적용해보자
const express = require("express"); // express 모듈
const router = express.Router(); 
const conn = require('../mariadb'); // db 모듈
const {StatusCodes} = require('http-status-codes'); // status 모듈

router.use(express.json()); // POST를 사용하면 값을 json형태로 받아오기 때문에 추가

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

  let sql = 'INSERT INTO users (email, password) VALUES (?, ?)'
  let values = [eamil, password]

  conn.query(sql, values,
    (err, results) => {
      if(err) {
        console.log(err);
        return res.status(StatusCodes.BAD_REQUEST).end // BAD REQUEST
      }
      res.status(StatusCodes.CREATED).json(results)
    })
  res.json('회원가입');
});
  • 잘 작동한다.



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

1️⃣ node.js 패키지(파일) 구조

  • app.js : 프로젝트의 메인 라우터 역할
  • routes
    • users.js : 하위 라우터 역할 = 경로를 찾아주자
    • books.js : 하위 라우터 역할
      ...

경로를 찾은 다음 역할 = '콜백함수'를 빼내자!

  • 라우터가 로직까지 다 수행할 때 단점
    • 프로젝트 규모가 커질수록 코드가 복잡해짐 👍해결방법 : 코드를 간결하고 가독성이 높에 만들자👍
    • 가독성이 안 좋음
    • 트러블 슈팅이 어려워짐(에러 찾기가 힘듦)
      👉 유지보수 하기 어려움

2️⃣ 컨트롤러란?

  • 프로젝트에서 매니저 역할을 하는 파일
  • 매니저는 누군가에게 일을 어떻게 시켜야 할 지 알고 있음 = 직접 일을 하지 않음

👉 router를 통해 "사용자의 요청(req)이" 길(url)을 찾아오면
👉 매니저(콜백함수 = controller)가 환영해줄 것임
👉 서비스에게 일을 시키고, 결과물을 매니저에게 전달 해주게 됨
👉 매니저(controller)가 사용자에게 res를 돌려줌

3️⃣ 컨트롤러를 만들어보자

  • users.js에서 콜백함수를 빼서 컨트롤러를 따로 만들어주자

  • 만들어준 UserController.js

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

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 // BAD REQUEST
      }

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

module.exports = join;
  • 간략해진 users.js
const express = require("express"); // express 모듈
const router = express.Router(); 
const conn = require('../mariadb'); // db 모듈
const join = require('../controller/UserController'); // 컨트롤러 불러옴

router.use(express.json()); // POST를 사용하면 값을 json형태로 받아오기 때문에 추가


// 회원가입
router.post('/join', join)
  • 콘트롤러에 콜백함수를 정의해주고 함수를 모듈로 내보내주면 라우터는 길 찾기만 하게 됨



3-4 로그인 api 구현 + unauthorized

1️⃣ controller를 정리해보자

  • 모든 API에 대해 콘트롤러를 만들어보자
  • controller.js에 로그인, 비밀번호 초기화 요청, 비민번호 초기화의 모듈을 만들고 json형태로 모듈을 내보내 주면
const {StatusCodes} = require('http-status-codes'); // 상태 모듈
const conn = require('../mariadb'); // db 모듈

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 // BAD REQUEST
      }

      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 {
  join, 
  login, 
  passwordResetRequest, 
  passwordReset
} = require('../controller/UserController'); // 컨트롤러 불러옴

router.use(express.json()); // POST를 사용하면 값을 json형태로 받아오기 때문에 추가


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


module.exports = router;

2️⃣ 로그인 API를 만들어보자

  • 아래와 같이 코드를 만들면 잘 작동한다.
const login = (req, res) => {

  const { email, password } = req.body; // 요청 본문에서 이메일과 비밀번호 추출

  let sql = 'SELECT * FROM users WHERE email = ?'; // 이메일을 기준으로 사용자를 조회하는 SQL 쿼리
  conn.query(sql, email, (err, results) => { // 데이터베이스 쿼리 실행
    if (err) { // 쿼리 실행 중 에러가 발생한 경우
      console.log(err); // 에러를 콘솔에 출력
      return res.status(StatusCodes.BAD_REQUEST).end(); // 클라이언트에 400 Bad Request 응답
    }

    const loginUser = results[0]; // 조회된 첫 번째 사용자 정보를 가져옴

    if (loginUser && loginUser.password == password) { // 사용자가 존재하고 비밀번호가 일치하는지 확인
      // 로그인 성공

      // JWT 토큰 발행
      const token = jwt.sign({
        email: loginUser.email // 토큰에 포함할 사용자 정보 (이메일)
      }, process.env.PRIVATE_KEY, { // 비밀 키를 이용해 토큰을 서명
        expiresIn: '5m', // 토큰 만료 시간 설정 (5분)
        issuer: 'kim' // 토큰 발행자 정보
      });

      // JWT 토큰을 쿠키에 담아서 클라이언트에게 전달
      res.cookie("token", token, {
        httpOnly: true // 쿠키는 클라이언트의 JavaScript에서 접근할 수 없도록 설정 (보안 강화)
      });
      console.log(token); // 생성된 토큰을 콘솔에 출력

      return res.status(StatusCodes.OK).json(results); // 로그인 성공 시 200 OK와 함께 결과 응답
    } else {
      // 로그인 실패 (사용자가 없거나 비밀번호가 틀린 경우)
      return res.status(StatusCodes.UNAUTHORIZED).end(); // 401 Unauthorized 응답
    }
  });  
};



3-5 비밀번호 초기화 요청 & 초기화

1️⃣ 비밀번호 초기화 요청 API 만들기

// 비밀번호 초기화 요청 함수 정의
const passwordResetRequest = (req, res) => {
  // 클라이언트가 보낸 요청 본문에서 이메일을 추출
  const { email } = req.body;

  // 이메일을 기준으로 사용자를 조회하는 SQL 쿼리문 작성
  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(); // 클라이언트에 400 Bad Request 응답
    }

    // 쿼리 결과에서 첫 번째 사용자 정보를 가져옴
    const user = results[0];

    // 사용자가 존재하는 경우
    if (user) {
      return res.status(StatusCodes.OK).json({
        email: email // 성공적으로 조회된 이메일을 JSON으로 응답
      });
    } else {
      // 사용자가 존재하지 않는 경우
      return res.status(StatusCodes.UNAUTHORIZED).end(); // 401 Unauthorized 응답
    }
  });
};

2️⃣ 비밀번호 초기화API 만들기

  • 비밀번호 초기화 페이지에는 이메일 입력란이 없기 때문에 이전 초기화 요청 페이지에서 이메일을 가져와야 함(성공 시 이메일을 res해줌)
// 비밀번호 초기화 함수 정의
const passwordReset = (req, res) => {
  // 클라이언트가 보낸 요청 본문에서 email과 password를 추출
  const { email, password } = req.body;

  // 비밀번호를 업데이트하는 SQL 쿼리문 정의
  let sql = 'UPDATE users SET password = ? WHERE email = ?'; 
  // 쿼리에 바인딩할 값들 (password와 email)
  let values = [password, email];

  // 데이터베이스에 쿼리 실행
  conn.query(sql, values, (err, results) => {
    // 쿼리 실행 중 오류가 발생한 경우
    if (err) {
      console.log(err); // 에러를 콘솔에 출력
      return res.status(StatusCodes.BAD_REQUEST).end(); // 클라이언트에 400 Bad Request 응답
    }

    // 쿼리가 성공적으로 실행되었으나, affectedRows가 0인 경우 (해당 이메일이 존재하지 않는 경우)
    if (results.affectedRows == 0)
      return res.status(StatusCodes.BAD_REQUEST).end(); // 400 Bad Request 응답 (해당 이메일을 가진 사용자가 없음)
    else
      // 성공적으로 비밀번호가 변경된 경우
      return res.status(StatusCodes.OK).json(results); // 200 OK 응답과 함께 결과 반환
  });
};



3-6 회원가입 시 비밀번호 암호화

1️⃣ 비밀번호가 날 것으로 들어가면 안됨

  • 데이테 베이스를 보면 비밀번호를 그대로 저장해둠 이렇게 저장하면 털리면 끝...

  • 패스워드 암호화 모듈을 배워보자!

2️⃣ 회원가입에 비밀번호 암호화 추가

  • crypto 모듈을 사용하여 랜덤바이트를 생성하고 문자열로 담아 암호화된 비번을 만들어보자
  // 비밀번호 암호화
  const salt = crypto.randomBytes(64).toString('base64');
  const hashPassword = crypto.pbkdf25ync(password, salt, 10000, 64, 'sha512').toString('base64')
  • 대략 이렇게 사용된다. 다른 곳에서 제대로 테스트 해보고 오자

3️⃣ node-base에서 테스트 해보자

  • 새롭게 암호화를 실행할 때마다 달라지는 게 보인다
const crypto = require('crypto');

const password = "1111";

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

console.log(hashPassword);

  • 암호화가 긴데 64글자가 아닌 10글자로 수정할 수도 있다.
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);

4️⃣ 암호화의 복구 방법

  • 그리고 암호화는 단방향이기 때문에 복구가 안된다... 매번 달라지는 암호화로 어떻게 로그인을 해야 되나?
    • salt가 랜덤 바이트를 가지고 오기 때문인데, salt를 고정하거나 salt를 데이터베이스에 저장하거나 두가지 방법이 있음

👉 회원가입 시 비밀번호를 암호화 해서 암호화된 비밀번호와 salt 값을 같이 저장한다.
👉 로그인 시 이메일 & 비밀번호(날 것)를 받아서 => DB에서 slat값 꺼내서 비밀번호 암호화 해보고 => DB에 저장된 비밀번호랑 비교한다.

  • 먼저, 워크벤치에서 users 테이블에 salt를 추가해준다.

  • 암호화된 비밀번호와 slat값을 DB에 함께 저장하도록 코드 수정

const {StatusCodes} = require('http-status-codes'); // 상태 모듈
const conn = require('../mariadb'); // db 모듈
const jwt = require('jsonwebtoken'); // jwt 모듈
const crypto = require('crypto') // 암호화 모듈
const dotenv = require('dotenv'); // env모듈
dotenv.config(); // 사용 선언

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

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

  // 암호화된 비밀번호와 slat값을 DB에 함께 저장함
  const salt = crypto.randomBytes(10).toString('base64');
  const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64')
  
  
  // 로그인 시 이메일 & 비밀번호(날 것)를 받아서 => DB에서 slat값 꺼내서 비밀번호 암호화 해보고 => DB에 저장된 비밀번호랑 비교한다.

  let values = [email, hashPassword, salt]; // 이메일과 해시패스워드, salt로 insert 해줌

  conn.query(sql, values,
    (err, results) => {
      if(err) {
        console.log(err);
        return res.status(StatusCodes.BAD_REQUEST).end // BAD REQUEST
      }
      return res.status(StatusCodes.CREATED).json(results)
    })
};
  • 회원가입이 잘 된다.



3-7 로그인은 어떻게? 비밀번호 초기화?

1️⃣ 로그인 시에도 암호화 적용

  • 로그인 시에도 암호화를 적용하여 로그인이 잘 되는지 확인한다.
// 로그인
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(); // BAD REQUEST
      }

      const loginUser = results[0];

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

      if (loginUser && loginUser.password == hashPassword) { // DB에 저장된 비밀번호랑 비교한다.

        // 토큰 발행
        const token = jwt.sign({
          email : loginUser.email
        }, process.env.PRIVATE_KEY, {
          expiresIn : '5m',
          issuer : 'kim'
        });

        //토큰 쿠키에 담기 = jwt 토큰을 바디로 보내지 않고 쿠키로 보냄
        res.cookie("token", token, {
          httpOnly : true
        });
        console.log(token);

        return res.status(StatusCodes.OK).json(results)
      } else {
        return res.status(StatusCodes.UNAUTHORIZED).end(); // 401 : Unauthorized(미인증 상태), 403 : Forbidden(접근권한 없음)
      }
    });  
};

2️⃣ 비밀번호 초기화 로직에도 암호화를 적용해보자

// 비밀번호 초기화
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(); // BAD REQUEST
      }
      if(results.affectedRows == 0)
        return res.status(StatusCodes.BAD_REQUEST).end();
      else
        return res.status(StatusCodes.OK).json(results);
    }
  )
};



profile
김개발의 개발여정

0개의 댓글