프로젝트/도서구매사이트

1017 회원 API 구현 - 컨트롤러 분리, 비밀번호 암호화/복호화

thinktank911 2025. 10. 16. 17:18

회원가입 API 구현

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

  • http-status-code를 모듈 사용하여 출력하기
  • npm 설치 : npm install http-status-codes --save
const {StatusCodes} = require('http-status-codes');     // status code 모듈

// INSERT 쿼리문
conn.query(sql, values,
    function (err, results) {
        if(err){
            console.log(err)
            return res.status(StatusCodes.BAD_REQUEST).end();   // Bad Request(400)
        }

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

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

  • app.js : 프로젝트의 메인 라우터 역할
  • /routes
    • users.js : 하위 라우터 역할 = 경로 찾기 역할
    • books.js : 하위 라우터 역할
  • 콜백함수 분리 필요

※ 라우터가 로직까지 다 수행할 때 단점

1) 프로젝트 규모가 커질수록, 코드가 매우 복잡
2) 가독성x
3) 트러블슈팅 x
➡️  "유지보수 하기 어렵다"
cf. 유지보수란? 10년~ 운영! 요구사항 반영, 에러 해결,...

➡️  해결방법 : 코드를 간결하고 가독성 높게 만들어주기

 

컨트롤러

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

➡️ 라우터를 통해서
사용자의 요청(req)이 길(url)을 찾아오면
매니저(콜백함수 = controller)가 환영해줄 것
➡️ 알바생(서비스)에게 일을 시키고, 결과물을 매니저에게 전달
➡️ 매니저(controller)가 사용자에게 res 돌려준다.

 

회원가입 컨트롤러 분리

  • Controller/UserController.js 파일 생성
  • users.js 라우터에 있던 콜백함수 로직을 그대로 가져와 변수 선언해준다.
    (이때, 필요한 모듈 가져오는 것을 잊지 말아야 한다.
const conn = require('../mariadb')  // db 모듈 가져오기
const {StatusCodes} = require('http-status-codes');     // status code 모듈


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

        let sql = `INSERT INTO users (email, password, salt)
            VALUES (?, ?, ?)`;
        let values = [email, password ];
        // INSERT 쿼리문
        conn.query(sql, values,
            function (err, results) {
                if(err){
                    console.log(err)
                    return res.status(StatusCodes.BAD_REQUEST).end();   // Bad Request(400)
                }

                return res.status(StatusCodes.CREATED).json(results);  // 201
            }
        );
};
  • 모듈을 exports한다.
module.exports = {
    join,
    login,
    passwordResetRequest,
    passwordReset
};
  • users.js 라우터에서 해당 함수를 호출한다.
// express 모듈 셋팅
const express = require('express')
const router = express.Router() // 해당 파일을 express 라우터로 사용 가능
const conn = require('../mariadb')  // db 모듈 가져오기
const {StatusCodes} = require('http-status-codes');     // status code 모듈
const {body, param, validationResult} = require('express-validator') // 유효성검사 모듈(body: 내가만든 변수, validationResult : 에러 시 결과값)
const {
    join,
    login,
    passwordResetRequest,
    passwordReset} = require('../controller/UserController');

router.use(express.json()) // http 외 모듈 'json'

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

// 로그인
router.post('/login', login);

// 비밀번호 초기화 요청
router.post('/reset', passwordResetRequest);

// 비밀번호 초기화
router.put('/reset', passwordReset);

module.exports = router;

 

로그인 api 구현 + unauthorized

  • 로그인 시 아이디 또는 비밀번호가 일치하지 않을 때 어떤 status코드를 보내는 게 적합한지 고민해봤다.
    • 401 Unauthorized(미인증) : 접근한 사용자가 누군지 모른다.
    • 403 Forbidden(접근 권리 없음) : 사용자가 누군지 알지만 접근 권한이 없다.
  • 아이디, 비밀번호가 틀렸다면, 서버는 접근한 사람의 정체를 알지 못하니까 401 응답 메세지를 보내줬다.
else{
         res.status(StatusCodes.UNAUTHORIZED).json({  // 401 : Unauthorized(비인증) 403 : Forbidden(접근 권리 없음)
         message : "아이디 또는 비밀번호가 일치하지 않습니다."
   })
}

비밀번호 초기화 요청 & 초기화

  • 비밀번호 초기화 요청 시 이메일로 유저가 있는지 검색한 후 해당 이메일을 응답 json으로 넘겨준다.
  • 비밀번호 초기화 시 요청 때 받은 응답 json을 넘겨받아 req.body에 해당 이메일과 새 비밀번호를 셋팅
const passwordResetRequest = (req, res)=>{
    const {email} = req.body;
    let sql = `SELECT * FROM users WHERE email= ?`
    // SELECT 쿼리문
    conn.query(sql, email,
        function (err, results) {
            if(err){
                console.log(err)
                return res.status(StatusCodes.BAD_REQUEST).end()
            }

            // 이메일로 유저가 있는지 검색
            var loginUser = results[0];

            if(loginUser){
                // 확인
                res.status(StatusCodes.OK).json({
                    email : email
                });
            }
            else{
                res.status(StatusCodes.UNAUTHORIZED).end();
            }
        }
    )
};
const passwordReset = (req, res)=>{
    let {password, email} = req.body;

    let sql = `UPDATE users SET password = ? WHERE email = ?`

    let values = [ password , email];
    // UPDATE 쿼리문
    conn.query(sql, values,
        function (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{
                res.status(StatusCodes.OK).json(results);
            }
        }
    );
};

 

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

  • node에 내장되어 있는 기본 모듈인 crypto 모듈로 암호화하기
const crypto = require('crypto');       // crypto 모듈 : 암호화 (node.js 기본모듈)

// 비밀번호 암호화
// randomBytes(64) : 64만큼의 길이로 숫자를 랜덤바이트로 만들어줌
// toString('base64') : base64 방식으로 문자열화
const salt = crypto.randomBytes(64).toString('base64');
// pbkdf2Sync(해싱할 값, 해싱에 사용되는 무작위 문자열, 해시함수 반복횟수, 암호길이, 암호화알고리즘)
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');  // 해싱
  • randomBytes(n) : n 바이트의 암호화된 난수를 생성
  • toString('base64') : base64 방식으로 문자열로 변환
  • crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)
    • 인자설명 : password - 해싱할 값 / salt - 해싱에 사용되는 무작위 문자열
                      / iterations - 해시함수 반복횟수 / keylen - 암호길이 / digest -암호화알고리즘
※ 단방향 암호화의 문제점
- 이 방법의 경우, 단방향이라 복호화가 안 된다.
- 해결방법
➡️ salt를 데이터베이스에 저장하여 회원가입 시 암호화한 해싱 비밀번호와 salt 값을 같이 저장한다.
   로그인 시, 날 것의 비밀번호를 입력하면 salt값을 db에서 가져와 비밀번호를 암호화한 후, db 비밀번호랑 비교한다.
const join = (req, res)=>{
    const userInfo = req.body;
       
        const {email, password} = userInfo;

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

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

        // 회원가입 시 비밀번호를 암호화해서 암호화된 비밀번호와 salt 값을 같이 저장
        let values = [email, hashPassword, salt];
        // INSERT 쿼리문
        conn.query(sql, values,
            function (err, results) {
                if(err){
                    console.log(err)
                    return res.status(StatusCodes.BAD_REQUEST).end();   // Bad Request(400)
                }

                return res.status(StatusCodes.CREATED).json(results);  // 201
            }
        );
};
  • 회원가입 시 비밀번호를 해싱 암호화해서 암호화된 비밀번호와 salt 값을 같이 저장한다.
  • db 테이블 수정

 

로그인 시 암호화된 비밀번호 비교하기

  • 로그인 시 비밀번호를 입력하면 salt값을 db에서 가져와 비밀번호를 암호화한 후, db 비밀번호랑 비교한다.
const login = (req,res)=>{
    const {email, password} = req.body

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

            var loginUser = results[0];

            // salt값 꺼내서 날 것 비밀번호 암호화
            const hashPassword = crypto.pbkdf2Sync(password, loginUser.salt, 10000, 10, 'sha512').toString('base64'); 

            // 해시된 디비 비밀번호랑 비교
            if(loginUser && loginUser.password === hashPassword){
                // token 발급 / 유효기간 설정
                const token = jwt.sign({
                    email : loginUser.email
                }, process.env.PRIVATE_KEY, {
                    expiresIn : '30m',  // 유효시간
                    issuer : 'yj'       // 발행인
                });

                // 쿠키에 토큰 담기 - 토큰 변수에 토큰 담기
                res.cookie("token", token, {
                    httpOnly : true
                });
                console.log(token);

                res.status(StatusCodes.OK).json(results);
            }
            else{
                res.status(StatusCodes.UNAUTHORIZED).json({  // 401 : Unauthorized(비인증) 403 : Forbidden(접근 권리 없음)
                    message : "아이디 또는 비밀번호가 일치하지 않습니다."
                })
            }
        }
    )
};

비밀번호 초기화

  • 비밀번호 초기화 시 해싱을 다시 한다. salt도 새로 저장한다.
const passwordReset = (req, res)=>{
    let {password, email} = 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];
    // UPDATE 쿼리문
    conn.query(sql, values,

 


node 암호화 모듈인 crypto를 사용하여 해싱 암호화에 대해 배웠다. 단방향 암호화 알고리즘을 이용해 복호화 없이 어떻게 로그인을 할지 처음엔 판단이 서지 않았는데 해싱에 사용하는 salt값을 db에 저장해 로그인 시 꺼내서 암호화를 하고, 그 해싱 비밀번호를 db의 비밀번호 값과 비교한다는 점이 로직적으로 명확해서 좋았다.

라우터 파일 안에 로직이 들어가 있어 무겁다는 느낌이 들었는데 컨트롤러로 분리시킬 수 있어 전체적으로 코드가 깔끔해져 만족스럽다.