고양이와 코딩

[웹 풀사이클 데브코스 TIL] 8주차 Day 1 - API 설계, crypto를 사용한 암호화 본문

데브코스 TIL

[웹 풀사이클 데브코스 TIL] 8주차 Day 1 - API 설계, crypto를 사용한 암호화

ovovvvvv 2024. 1. 2. 19:17
728x90

http-status-codes

 if(err){
      return res.status(400).end()
 }

지금까지는 에러를 만나면 이런 식으로 status code를 보내줬는데요, 

사람은 누구나.. 언제나 실수할 수 있기 때문에 status code가 잘못 전달되서 프론트엔드 단에 혼란을 주기보다는 상태 코드를 변수에 담아서 보내면 좋을 것 같습니다!

 

이를 위해 npm에서 제공하고 있는 모듈이 있는데요 

const {StatusCodes} = require('http-status-codes');
   conn.query(sql, values,
        (err, results) => {
            if(err){
                return res.status(StatusCodes.BAD_REQUEST).end()
            }
            res.status(StatusCodes.CREATED).json(results)
        }
        );
});

이렇게 사용 할 수 있습니다 😺

 

 

파일을 분리해보자 !

프로젝트 규모가 커질수록 현재 코드처럼 라우터가 로직까지 다 수행하고 있다면, 유지보수와 확장성 측면에서 문제가 발생할 수 있습니다.

  1.  가독성과 유지보수 : 단일 파일에 모든 라우팅 로직이 포함되면 코드가 길어지고 복잡해지기 때문에, 코드를 이해하고 수정하기 어렵게 만들 수 있습니다.
  2.  확장성 : 프로젝트 규모가 커짐에 따라 더 많은 라우트와 관련된 로직이 추가 될 수 있습니다. 모든 라우트를 단일 파일에 넣으면 파일의 크기가 커지고, 관리가 어려워 질 수 있습니다.
  3.  모듈화와 재사용성 : 라우팅 로직이 모듈화되지 않으면 유사한 기능이 다른 엔드포인트에서 필요할 때 코드를 중복해서 작성해야 할 수 있습니다.

이러한 단점을 극복하기 위해 보다 구조화된 방식을 사용할 수 있습니다 (^∇^)

  • 라우트 분리: `/users`, `/books` ... 와 같이 관련된 엔드포인트를 별도의 파일로 분리하기
  • 모듈화 : 분리된 파일에서 관련 함수들을 작성하고 필요한 경우 해당 함수를 재사용할 수 있도록 구성하기
  • MVC 구조 도입: 모델, 뷰, 컨트롤러와 같은 구조를 도입하여 코드를 더 관리하기 쉽게 만들기 (컨트롤러 사용하기)

 

하나의 예를 들어볼게요, 기존에 작성했던 routes 폴더 안의 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()
            }
            res.status(StatusCodes.CREATED).json(results)
        }
        );
});

이렇게 로직 + 라우터 역할을 다 하고 있었는데요

 

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());

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

 

controller를 만들어서 

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()
            }
            res.status(StatusCodes.CREATED).json(results)
        });
    };

module.exports = join

그 안에 로직을 구현하면!

 

이제 라우터는 라우터의 역할만을 하고 있고, 콜백함수 자리에 join 모듈을 넣어 그 다음 역할은 userController.js로 넘기고 있어요 ㅎㅎ

 

 

혼자서 하나 더! 해보기

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();
            }
    })
};
    
    
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);
        }
    })
};

위 코드는 비밀번호 초기화 요청과 비밀번호 초기화 인데요, 비밀번호를 초기화 할 때, 
새로 입력한 비밀번호가 기존 비밀번호와 똑같다면 오류 메세지를 보여주도록 한번 짜 볼게요 !!

 

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();
            }
    })
};
    
    
const passwordReset = (req, res) => {
   const {email, password} = req.body;

// 이전 비밀번호 가져오기
   let sql = `SELECT password FROM users WHERE email = ?`;

   conn.query(sql, email, 
    (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();
        }

        const beforePassword = results[0].password;

        if (beforePassword == password) {
            return res.status(StatusCodes.BAD_REQUEST).json({
                message : "비밀번호가 이전과 동일합니다. 다른 비밀번호를 입력해주세요."
            })
        } else {
            // 새 비밀번호로 업데이트
            let updateSql = `UPDATE users SET password = ? WHERE email = ?`;
            let values = [password, email];

            conn.query(updateSql, values,
                (updateErr, updateResults) => {
                    if (updateErr) {
                        console.log(updateErr);
                        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(updateResults);
                    }
                })
        }
    })
};

기존의 비밀번호인 1111을 그대로 입력했더니 ㅎㅎ 원하던 결과가 나왔습니다 !

 

변경도 잘 된걸 확인할 수 있어요

 

여기서 변경되기 이전의 비밀번호를 받아와야 하는데 처음 몇 번 이미 변경된 비밀번호를 받아오게끔 로직을 잘못 짜서
삽질을 또 조금(?) 했습니다 ㅎㅎ 
그래도 또 하나 배웠네요 !

 

Crypto 모듈 사용해서 암호화를 해보자

'crypto' 모듈은 Node.js에서 제공하는 내장 모듈 중 하나로, 다양한 암호화 기능을 제공합니다!

주로 데이터의 보안을 위해 해시(hashing), 암호화(encryption), 복호화(decryption), 서명(signing), 그리고 무작위 바이트 생성

(random bytes generation) 등의 기능을 제공합니다 (ؑᵒᵕؑ̇ᵒ)◞✧

또한,, 복호화는 되지 않습니다 (단방향 ㅎㅎ;;)

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

 

요 코드는 비밀번호를 안전하게 저장하기 위해 해시 함수를 사용하여 주어진 비밀번호를 암호화 하고, 이를 안전한 방법으로

저장할 수 있도록 하는 예시입니다 ㅎㅎ

'pbkdf2Sync()' 메서드는 비밀번호 기반 키 도출 함수로, 주어진 비밀번호를 해시화 하여 저장될 때 보다 안전하게 만들어 줍니다 !

 

사용 예시 ~

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

console.log(hashPassword);

이렇게 암호화 된 문자의 길이를 정해 줄 수도 있고, 암호화 된 값은 계속해서 바뀝니다 (⌒◞⌒)

 

따라서, 어떻게 암호화 된 비밀번호를 구분할것이냐 ... 하면

회원가입을 할 때 비밀번호를 암호화해서 암호화 된 비밀번호와, salt값을 같이 db에 저장한다
로그인 시, 이메일과 비밀번호(암호화상태가 아닌)를 받을텐데 => salt값을 꺼내서 비밀번호를 암호화 해보고
=> db에 저장된 비밀번호와 비교해서 맞는지 확인한다
const join = (req, res) => {
    const {email, password} = req.body;

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


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

    console.log(hashPassword);

    // 로그인 시, 이메일 & 비밀번호 (날것의)를 받을텐데, => salt 값 꺼내서 비밀번호를 암호화 해보고
    // => 디비에 저장된 비밀번호와 비교해서 맞는지

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

요렇게요 !

 

 

 

 

ㅎㅎ 한 80퍼센트만 이해한 상태에서 혼자 비밀번호가 같은 경우 코드짜기 + 거기에 암호화 더하기
를 하니까 한 반절만 이해한 사람이 되어버렸습니다... 해시 부분은 무조건 더 공부해야 할 것 같아요!!!
암호화가 된 상태에서 같은 비밀번호를 입력하면 오류가 발생하지 않기 때문에 ... ㅋ훔