개발을 진행하면서 CRUD만 작성을 하다보니깐 에러를 처리하는 부분이 마음에 들지 않아서!!
특히, catch부분에 ctrl + c , ctrl + v 로 500에러만 나타내주기 보다는 각각의 상황에 맞춰 에러를 핸들링 해주고 싶었습니다.
 구글링을 통해서 best practice를 찾아보고 적용하면서 부족한 부분이 무엇인지에 대해서도 알 수 있었습니다.
next()의 동작과 app.js의 플로우와 app.use를 세밀하게 더 살펴볼 수 있었고 제 코드에 적용할 수 있었던 기회였습니다!

controlloer/user.controller.js

const userService = require('../services/user.service');
const HttpStatusCode = require('../config/httpStatusCode');

module.exports = {
    findAllUser: async (req, res) => {
        try {
            const user = await userService.read();
            if (user.length !== 0) return res.status(400).send({ statusCode: 400, message: "Bad Request" });
            return res.status(200).send({ data: user, statusCode: 200 });
        } catch (error) {
            res.status(500).send({ statusCode: 500, message: "Server Error" });
        }
    }
}

 

결과  

위의 controller의 에러 처리

구글링을 통해 Error Best Practice 찾아봐서 내 코드에 적용해보기

config/httpStatusCode.js

const HttpStatusCode = {
    OK: 200,
    BAD_REQUEST: 400,
    NOT_FOUND: 404,
    INTERNAL_SERVER: 500,
}

module.exports = HttpStatusCode;

error/error.js

const HttpStatusCode = require('../config/httpStatusCode');

class BaseError extends Error {

    constructor(name, httpCode, description, isOperational) {
        super(description);
        Object.setPrototypeOf(this, new.target.prototype);

        this.name = name;
        this.httpCode = httpCode;
        this.isOperational = isOperational;

        Error.captureStackTrace(this);
    }
}
//free to extend the BaseError
class APIError extends BaseError {
    constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') {
        super(name, httpCode, isOperational, description);
    }
}

module.exports = { BaseError, APIError };

 

 

 

middlewares/errorHandler.js

function logError(err) {
    console.error(err)
}

function logErrorMiddleware(err, req, res, next) {
    logError(err)
    next(err)
}

function returnError(err, req, res, next) {
    res.status(err.statusCode || 500).send(err.message)
}

function isOperationalError(error) {
    if (error instanceof BaseError) {
        return error.isOperational
    }
    return false
}

module.exports = {
    logError,
    logErrorMiddleware,
    returnError,
    isOperationalError
}

자 그러면, 기존의 Controller에 적용을 하기 전에 app.js에서 middleware를 app.use()를 통해 등록해줍니다.

const express = require('express');
const userRouter = require('./routes/userRouter');
const logger = require('./logger/logger');
const { APIError } = require('./errors/error');
const { logError, returnError, logErrorMiddleware } = require('./middlewares/error1');


const app = express();

app.use(express.json());

app.use('/user', userRouter);
app.use(APIError);
app.use(logError);
app.use(logErrorMiddleware)
app.use(returnError);
app.use(logHandler);

app.listen(3030, () => {
    logger.info('server is running!');
});

Controller에 적용해보기

const userService = require('../services/user.service');
const HttpStatusCode = require('../config/httpStatusCode');
const { APIError } = require('../errors/error');

module.exports = {
    findAllUser: async (req, res, next) => {
        try {
            const user = await userService.read();
            if (user.length !== 0) throw new APIError("Bad Request", HttpStatusCode.BAD_REQUEST, true, "Bad Request");
            return res.status(200).send({ data: user, statusCode: 200 });
        } catch (error) {
            next(error);
        }
    }
}

 

결과 

예제 적용한 후의 결과
log

참고 사이트 : https://sematext.com/blog/node-js-error-handling/

자료구조 Hash에서 정리했듯이 crypto를 활용하여 비밀번호를 암호화 하는 방식에 대해서  왜 사용을 하였는지에 초점을 두고 정리를 하고자합니다.

솔트(Salt)란?

암호학에서 솔트는 데이터, 비밀번호, 통과암호를 해시 처리하는 단방향 함수의 추가 입력으로 사용되는 랜덤 데이터입니다.

솔트(Salt)는 왜 사용한 것인가요?

예를 들어, 'jeong-park' 이라는 스트링을 sha512로 해시 해보겠습니다.
"2C1E444BE95BCB0236B7C82FDA667EC441B7A7E087898EA3299512675FADB1D65106905CEF0F757CB0D96A1AB82F97B83D3C64A8F30052893E6A8EC46D5935AA" 
위처럼 나옵니다. 제가 아닌 다른 사람들이 'jeong-park'이라는 스트링을 입력하여 sha512로 해시하면 똑같이 나옵니다.

항상 똑같은 결과값을 반환한다면?
해시 함수를 사용하여 변환 가능한 모든 해시 값을 저장시켜 놓고 저장시켜 놓으면 원래의 비밀번호를 추출(레인보우 테이블)해낼 수 있지 않을까요?
이러한 문제 때문에 Salt을 사용하며, 또한 Key Stretching 이라는 친구를 사용합니다.
 
* Key Stretching은 해시된 값을 또 다시 해시 함수의 입력 값으로 넣어서 해싱을 반복하는 것입니다. 이런 방식으로 무작위 대입을 통해 해시 값을 구하는 시간을 늘려서 레인보우 테이블이나 무차별 대입 공격을 어렵게 하기 위함이라고 합니다.

1. createSalt : randomBytes 메소드로 64바이트 길이의 salt를 생성합니다. new_salt는 버퍼형식으로 나오기 때문에 toString("base64")을 사용하여 리턴해줍니다.
2. createHashedPassword : pbkdf2(유저가 입력한 비밀번호, 만들어준 솔트값, 키스트래칭, 비밀번호의 길이, 해시함수) 입니다. 

(유저가 입력한 비밀번호 + 솔트 값) => Hash 값을 * Key Stretching 만큼 반복 하여 = HashedPassword를 만들어 줍니다.

const crypto = require('crypto');
const util = require('util');

const pbkdf2Promise = util.promisify(crypto.pbkdf2);
const randomBytesPromise = util.promisify(crypto.randomBytes);

module.exports = {

    /**
     * Salt Create 
     * @returns String
     */
    createSalt: async () => {
        const new_salt = await randomBytesPromise(64);
        return new_salt.toString("base64");
    },

    /**
     * Create HashedPassword
     * @param {String} password 
     * @returns String, String
     */
    createHashedPassword: async (password, currnet_salt) => {
        const salt = currnet_salt;
        const key = await pbkdf2Promise(password, salt, Number(process.env.KEY_STRETCHING), 64, "sha512");
        const hashedPassword = key.toString("base64");
        return { hashedPassword, salt };
    },

    /**
    * verifyPassword
    * @param {String} password 
    * @param {String} userSalt 
    * @param {String} userPassword 
    * @returns boolean
    */
    verifyPassword: async (password, userSalt, userPassword) => {
        const key = await pbkdf2Promise(password, userSalt, Number(process.env.KEY_STRETCHING), 64, 'sha512');
        const hashedPassword = key.toString('base64');
        if (hashedPassword === userPassword) return true;
        return false;
    }
}

 

참고 사이트 : https://ko.wikipedia.org/wiki/%EC%86%94%ED%8A%B8_(%EC%95%94%ED%98%B8%ED%95%99)

상황 

npm install faker

를 한 후, faker를 사용 후 적용을 했더니 Error : faker.js : NotFound

이유
The developer of these libraries intentionally introduced an infinite loop that bricked thousands of projects that depend on ‘colors’ and ‘faker.’  

-> 라이브러리의 개발자는 의도적으로 '색상' '페이커' 의존하는 수천 개의 프로젝트를 망가뜨리는 무한 루프를 도입했습니다.

해결 

npm install faker@5

구버전으로 버전을 바꿔준 후 해결 할 수 있었습니다.

'개발일지 > Troubleshooting' 카테고리의 다른 글

[Header설정 오류] - 협업 중..  (0) 2023.04.19


  토큰을 발행해준 후, verify 과정에서 Invalid, expired를 처리해줘야 할까 고민이 들었습니다. 그래서 상황에 맞는 시나리오를 그리고 어떻게 해결해 나갈 것인지 코드를 작성하였습니다.

시나리오

1. 로그인을 하면 AccessToken과 RefreshToken을 발행합니다.
 - AccessToken은 Bearer, RefreshToken은 Redis에 저장을 해줍니다.

2. 사용자가 인증일 필요한 API에 접근하고자 할 때, 검증 미들웨어(authMiddleware)를 통해 검사함.
 

  • Case 1 : AccessToken 만료, RefreshToken 만료  : Error
  • Case 2 : AccessToken 만료, RefreshToken 유효 : AccessToken 재발급
  • Case 3 : AccessToken 유효, RefreshToken 만료 : 로그인 재요청
  • Case 4 : AccessToken 유효, RefreshToken 유효 : next

3. 로그아웃

  • AccessToken, RefreshToken 삭제시키기.

🛠Advanced Feacture🛠

const authMiddleware = {

    checkToken: async (req, res, next) => {

        try {
            // accessToken이 만료된 경우에는 새로 발급 받은 accessToken을 받아옵니다. 그 외에는 null로 넘겨줬습니다.
            const { accessToken, result } = await authService.verifyToken(req);
			
            //result를 통해서 ( "(AccessToken||RefreshToken)_(INVALID||EXPIRED)" 를 받아옵니다.
            
            //AccessToken이 변조된 경우
            if (result === ACCESSTOKEN_INVALID) {
                return res.status(403).send({ err: "accessToken is invalid", statusCode: 403, msg: 'Forbidden' });
            }
            //RefreshToken이 변조된 경우
            else if (result === REFRESHTOKEN_INVALID) {
                return res.status(401).send({ err: "refreshToken is invalid", statusCode: 401, msg: '재로그인 해주세요' });
            }
            //AccessToken이 만료된 경우            
            else if (result === ACCESSTOKEN_EXPIRED) {
            	//새로 받아온 accessToken을 Bearer + 새로 발급받은 accessToekn을 넣어줍니다.
                res.header('AccessToken', "Bearer " + accessToken);
                return next();
            }
            //RefreshToken이 만료된 경우
            else if (result === REFRESHTOKEN_EXPIRED) {
                return res.status(401).send({ statusCode: 401, msg: '재로그인 해주세요' });
            }
            //둘다 유효한 경우에는 기존의 accessToken을 넘겨줍니다.
            res.header('AccessToken', "Bearer " + accessToken);

            return next();
        } catch (error) {
        	//둘다 만료인 경우에는 에러
            error.status = 500;
            error.msg = 'token is not found';
            next(error);
        }
    }
}

 

 

 

플로우

코드를 작성해 나가면서 위의 플로우처럼 모든 시나리오를 수행하는 방법도 있을 수 있지만, 다른 방법이 있지 않을까라는 생각이 들었습니다.  

예를 들어, 토큰이 만료되었다고 클라이언트 측에서 재발급 요청을 한다거나 하는 그런 플로우도 가능하지 않을까 생각을 했습니다.
하지만, 그렇게되면 불필요한 요청이 많아지는 것을 확인할 수 있었습니다.

상황 
  로그인 시,  Front에 AccessToken을 전달해줬지만, 다른 페이지를 요청할 때 AccessToken이 넘어오지 않은 경우.

이유
  프론트에서 header를 설정해 주지 않았기 때문에 local storage에만 저장이 되고 전달이 되지 않았습니다.
 

해결 

axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;

프론트에서 서버로 토큰을 보낼때 꼭 헤더를 설정하는걸 잊지 말도록 합시다!

'개발일지 > Troubleshooting' 카테고리의 다른 글

[Faker Error]  (0) 2023.04.19

+ Recent posts