0212 인증 관련 로직 작성하기

Younha Lee·2026년 2월 12일

TIL

목록 보기
30/66

회원가입 api

router.post(
    '/register',
    [
    body('email').notEmpty().isEmail().withMessage('email 형태로 입력해주세요'),
    body('password').notEmpty().isString().withMessage('password를 입력해주세요'),
    ],
     (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }
    const {email, password} = req.body;
    const sql = `INSERT INTO users (email, password) VALUES (?,?)`;
    const values = [email, password];
    db.query(sql, values,
        (err, results) => {
        // 입력값 검증했기 때문에 상태코드 500으로 반환
        if (err) return res.status(400).json({ msg: err.message });
        return res.status(201).json(results)
        });
})

이 코드에서 가장 아쉬운 건 상태코드의 하드 코딩이에요.
NestJS에서는 throw new NotFoundException(e.message); 등으로 처리하던 것을 생각하고 express를 구현하니 res 을 직접 사용하는 게 어색했어요.

http-status-codes


NestJS처럼 이미 상수화된 에러 코드를 해당 라이브러리를 import해서 사용할 수 있어요.

    db.query(sql, values,
        (err, results) => {
        if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ msg: err.message });
        return res.status(StatusCodes.CREATED).json(results)
        });

라이브러리를 적용하니, 하드코딩을 없앨 수 있었어요.

DB도 잘 연결되어있고, 쿼리도 잘 날아간 것을 볼 수 있어요.

라우터 콜백함수 분리(회원가입)

라우터와 컨트롤러를 분리해야할 필요를 느꼈어요.
라우터는 url에 따른 요청만 컨트롤러에게 넘겨주는 역할이 맞다고 생각했어요.
컨트롤러를 따로 파일을 만들어, 라우터의 콜백함수를 옮겨놨어요.

import {validationResult} from "express-validator";
import db from "../db.js";
import {StatusCodes} from "http-status-codes";

export const register = (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({errors: errors.array()});
    }
    const {email, password} = req.body;
    const sql = `INSERT INTO users (email, password)
                 VALUES (?, ?)`;
    const values = [email, password];
    db.query(sql, values,
        (err, results) => {
            // 입력값 검증했기 때문에 상태코드 500으로 반환
            if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({msg: err.message});
            return res.status(StatusCodes.CREATED).json(results)
        });
};

이렇게 UsersController.js 를 분리하고, 라우터에는 export 한 컨트롤러만 붙였어요.

import { register } from '../controllers/UsersController.js';

router.post(
    '/register',
    [
    body('email').notEmpty().isEmail().withMessage('email 형태로 입력해주세요'),
    body('password').notEmpty().isString().withMessage('password를 입력해주세요'),
    ],
     register,
);

이렇게 보니 validator도 미들웨어를 따로 분리하자고 생각했어요.
validators/userValidator.js 를 따로 만들었어요.

import { body, validationResult } from 'express-validator';

export const validateRegister = [
    body('email').isEmail().withMessage('이메일 형식을 확인해주세요.'),
    body('password').notEmpty().withMessage('비밀번호를 입력해주세요.'),
    (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        next(); // 에러가 없으면 다음 단계(컨트롤러)로 이동
    }
];
router.post(
    '/register',
    validaeRegister,
     register,
);

훨씬 깔끔하게 바뀌었어요.

로그인

export const login = (req, res) => {
    const {email, password} = req.body;
    const sql = 'SELECT * FROM users WHERE email = ?';
    const values = [email];
    db.query(sql, values, (err, results) => {
        if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({msg: err.message});

        const user = results[0];
        if (user && user.password === password) {
            const token = jwt.sign({
                id: user.id
            }, process.env.JWT_SECRET,
                {
                    expiresIn: '1d',
                    issuer: 'bookstore-api',
                });
        }
        res.cookie("access_token", token, {
            httpOnly: true,
        });
      
        return res.status(StatusCodes.OK).json(results)
    });

비밀번호가 틀렸을 때 401 UnAuthorized를 반환하도록 했어요.

비밀번호 초기화 요청

export const passwordResetRequest = (req, res) => {
  const { email } = req.body;
  const query = 'SELECT * FROM users WHERE email = ?';
  const values = [email];
  db.query(query, values, (err, results) => {
      if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
      const user = results[0];
      if (user) {
          return res.status(StatusCodes.OK).json({
              email: user.email
          });
      } else {
          return res.status(StatusCodes.NOT_FOUND).end();
      }
  });
};

이 곳도 이메일로 유저를 찾아내면 200, 아니면 404를 내게 했어요.
비밀번호 초기화 form에 이메일을 넣지 않기 때문에 클라이언트가 기억할 수 있게 이메일을 응답에 넣어줬어요.

비밀번호 초기화

export const passwordReset = (req, res) => {
    const { email, password } = req.body;
    const query = 'UPDATE users SET password = ? WHERE email = ?';
    const values = [password, email];
    db.query(query, values, (err, results) => {
        if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
        if (results.affectedRows === 0) return res.status(StatusCodes.NOT_FOUND).end();
        return res.status(StatusCodes.OK).json(results);
    });
};

UPDATE 문을 사용해 비밀번호를 변경하도록 했어요.
이때 affectedRow가 0이면, 해당 이메일로 유저를 못 찾은 것이니 404를 반환했어요.
요청 형식의 문제는 아니니 절대 인강에서 작성하는 400은 아니라고 생각했어요.

비밀번호 암호화

드디어 평문으로 된 비밀번호를 암호화하기로 했어요.
노드는 bcrypt 를 사용하여 할 수 있어요.
하지만 인강을 따라 우선 crypto 자체 모듈을 사용해볼게요.

const salt = crypto.randomBytes(64).toString('base64');
const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString();

이 암호화는 salt가 매번 바뀌니까 salt 또한 db에 저장해야해요.

export const register = (req, res) => {
    const {email, password} = req.body;
    const salt = crypto.randomBytes(10).toString('base64');
    const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({errors: errors.array()});
    }
    const sql = `INSERT INTO users (email, password, salt)
                 VALUES (?, ?, ?)`;
    const values = [email, hashPassword, salt];
    db.query(sql, values,
        (err, results) => {
            if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({msg: err.message});
            return res.status(StatusCodes.CREATED).json(results)
        });
};


passwordsalt 가 무사히 저장된 것을 볼 수 있어요.

로그인 코드 수정

export const login = (req, res) => {
    const {email, password} = req.body;
    const sql = 'SELECT * FROM users WHERE email = ?';
    const values = [email];
    db.query(sql, values, (err, results) => {
        if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
        const user = results[0];
        // salt를 꺼내 들어온 비밀번호를 암호화하고 db의 비밀번호와 비교
        const hashPassword = crypto.pbkdf2Sync(password, user.salt, 10000, 10, 'sha512').toString('base64');
        if (user && user.password === hashPassword) {
            const token = jwt.sign({
                id: user.id
            }, process.env.JWT_SECRET,
                {
                    expiresIn: '1d',
                    issuer: 'bookstore-api',
                });
            res.cookie("access_token", token, {
                httpOnly: true,
            });
        } else {
            return res.status(StatusCodes.UNAUTHORIZED).end();
        }
        return res.status(StatusCodes.OK).json(results)
    });
};

같은 salt를 친 비밀번호와 db 속 비밀번호를 비교하게 했어요.

무사히 로그인이 잘 된 것을 볼 수 있어요.

비밀번호 변경

export const passwordReset = (req, res) => {
    const { email, password } = req.body;
    const salt = crypto.randomBytes(10).toString('base64');
    const hashPassword = crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
    const query = 'UPDATE users SET password = ?, salt = ? WHERE email = ?';
    const values = [hashPassword, salt, email];

    db.query(query, values, (err, results) => {
        if (err) return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: err.message});
        if (results.affectedRows === 0) return res.status(StatusCodes.NOT_FOUND).end();
        return res.status(StatusCodes.OK).json(results);
    });
};

비밀번호 변경 api도 salt와 해싱을 추가했어요.
salt와 변경된 password 모두 db에 새로 넣어주는 것으로 마무리했어요.

profile
할 땐 하고 놀 땐 노는 일일놀놀입니다.

0개의 댓글