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

1029 headers의 Authorization으로 jwt 인증 구현, try~catch 예외처리

thinktank911 2025. 10. 29. 11:45

jwt 토큰 인증

  • 로그인 할 때 발급해 cookie에 넘겨준 jwt 토큰을 header에서 꺼내 사용한다.

jwt api 테스트

  • res.cookie에 담아보내기 ➡️ response Headers의 Set-Cookie에 담긴다.
  • 지난 쿠키값 그대로 받아 다음 api 실행 시 req.cookie에 그대로 씀
// GET + "/jwt" : 토큰 발행
app.get('/jwt', (req, res) => {
  var token = jwt.sign({
    username : 'kim'
  }, process.env.PRIVATE_KEY,
  {
    expiresIn: '1m',
    issuer: 'admin'
  });

  // 토큰 쿠키에 담기
  res.cookie("jwt", token, {
    httpOnly: true
  });
  console.log(token);
  res.send("토큰 발행 완료!");
})

res.cookie ➡️ response Headers의 Set-Cookie에 담는다.

다음번 API 실행 시 req headers의 cookie에 발급해준 토큰이 담겨 있다.

authorization 받아보기

  • cookie에 담아 보내준 토큰을 복호화 시 header의 authorization 키에 넣는다.
  • req.headers["authorization"]으로 해당 토큰을 읽는다.
  • 복호화 한다.
// GET + "/jwt/decoded" : 토큰을 검증
app.get('/jwt/decoded', (req, res) => {
  let receivedJwt = req.headers["authorization"];
  console.log(receivedJwt);
  var decoded = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
 
  res.send(decoded);
})


좋아요 추가 api에 jwt 구현

  • 로그인 시 토큰 발행 : 페이로드에 db에서 가져온 id와 email 넣기
  • 토큰 복호화해서 id가져오기 : decodedJwt.id를 user_id 대신 values에 넣어주면 된다.
// token 발급 / 유효기간 설정
const token = jwt.sign({
   id : loginUser.id,
   email : loginUser.email
}, process.env.PRIVATE_KEY, {
     expiresIn : '1m',  // 유효시간
     issuer : 'yj'       // 발행인
});
let receivedJwt= req.headers["authorization"];
        console.log("received jwt: ", receivedJwt);

        let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
        console.log(decodedJwt);
       
        // 좋아요 추가
        let sql = `INSERT INTO likes (user_id, liked_book_id) VALUES (?, ?)`;
        let values = [decodedJwt.id, book_id];

좋아요 취소 API 구현, user_id 꺼내기 모듈화

  • 좋아요 취소 API에도 인증 복호화 코드를 작성하려니 좋아요 추가 API 안에서의 코드와 겹친다.
    ➡️ 함수로 인증 복호화 코드를 모듈화하여 재사용
const removeLike = (req, res)=>{
    let book_id = req.params.id;

    // 인증 복호화
    let authorization = ensureAuth(req);

    let sql = `DELETE FROM likes WHERE user_id = ? AND liked_book_id = ?`;
    let values = [authorization.id, book_id];
});

.
.
.
// 인증 복호화
function ensureAuth(req){
    let receivedJwt= req.headers["authorization"];
    console.log("received jwt: ", receivedJwt);

    let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
    console.log(decodedJwt);
    return decodedJwt;
}

장바구니 API에서 jwt 구현

  • 장바구니 API에서 jwt 인증을 구현하려는데 다음과 같은 문제가 발생했다.
1) jwt expired
2) ensureAuth가 파일마다 반복된다.
3) 장바구니 아이템 목록 조회 = 내 장바구니 보기
   ➡️ user_id로 회원의 장바구니를 전부 보여줘야 하는데 현재는 selected 없이 보내면 안된다.
   ➡️선택한 장바구니 목록 조회와 분리 필요
  • 위 문제를 하나씩 해결해보고자 한다.

jwt expired 에러 컨트롤

  • jwt 유효기간 지나면 현재 500 서버 에러가 나면서 서버가 종료된다.
  • 이때, 예외(개발자가 생각하지 못한 에러) 처리를 통해 유효기간이 지났을 경우,
    res ➡️  '로그인(인증) 세션(로그인 유지 상태)이 만료되었습니다.' 메세지를 출력하도록 해주어야 한다.

jwt 예외 처리 종류

1) jwt.TokenExpiredError

  • 유효기간이 지난 토큰 = 만료된 토큰

2) jwt.JsonWebTokenError

  • 문제 있는 토큰

try ... catch

  • 개발자가 예상하지 못한 에러를 처리하는 문법
  • 실수, 사용자가 입력을 잘못한 것, 디비가 응답을 잘못한 것...etc  ➡️ 프로그램에서 발생하는 수많은 에러들
  • if/else를 쓰기에는 수많은 에러의 경우의 수를 파악하기 어렵다
    ➡️ 실수1, 실수2... 비슷한 분류들끼리 묶어서 누군가.. 따로 관리를 해주면 좋겠다.
// if ~ else 구문
코드 A;
if (A에서 발생한 실수1){

}else if(A에서 발생한 실수2){

}

// try ~ catch 구문
try{
코드 A;
} catch (err) {
// 에러 처리
}
  • try 구문의 코드를 실행하다가 에러가 발생하면, try 코드를 멈추고 catch로 err와 함께 바로 빠져나간다.
  • try 구문에서 어떤 에러가 발생해도 if문 분기 처리를 해주던 내용들이 알아서 catch에 잡힌다.
    ex. SyntaxError, TypeError => 에러 객체

에러 객체

  • 자바스크립트가 많이 발생하는 에러를 토대로 내장 에러 객체를 만들었다.
  • JWT 모듈에서 제공하는 에러 객체 + 직접 만들 수도 있다.
console.log(err.name); // 에러 객체의 이름
console.log(err.message); // 에러 객체 메세지

// (출력 예시)
TokenExpiredError
jwt expired

throw 연산자

  • 에러를 발생시키는 연산자 ➡️ throw 에러 객체
    ex. throw new SyntaxError();
let error = new Error("대장 에러 객체");
let syntaxError = new SyntaxError("구문 에러 객체");
let referencedError = new ReferenceError("대입 에러 객체");

console.log(error.name);
console.log(error.message);

console.log(syntaxError.name);
console.log(syntaxError.message);

console.log(referencedError.name);
console.log(referencedError.message);
Error
대장 에러 객체
SyntaxError
구문 에러 객체
ReferenceError
대입 에러 객체
  • if는 구문을 다 읽는데 try catch는 문제가 발생했을 때 throw를 사용하면 안 읽고 catch로 넘겨줌
let string = '{ "num1": 1}'

try{
    // username;
    let json = JSON.parse(string);
    if(!json.name){
        throw new SyntaxError("입력 값에 이름이 없습니다.");
    }else{
        console.log(json.name); // js 입장에선 에러가 아니지만, 우리 입장에선 에러! = 입력값이 잘못된 에러
    }

    let name = json.name;
    console.log(json.name);
 }catch(err){
    // 먼저 잡힌 에러를 먼저 반환한다.
    console.log(err.name);
    console.log(err.message);
}
SyntaxError
입력 값에 이름이 없습니다.

TokenExpiredError 발생 시 에러 처리

// 인증 복호화
function ensureAuth(req, res){
    try {
        let receivedJwt= req.headers["authorization"];
        console.log("received jwt: ", receivedJwt);

        let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
        console.log(decodedJwt);
        return decodedJwt;
    } catch (err) {
        console.log(err.name);
        console.log(err.message);

        return res.status(StatusCodes.UNAUTHORIZED).json({
            "message" : "로그인 세션이 만료되었습니다. 다시 로그인하세요."
        });
    }
   
}
  • 해당 코드에서 유효기간 만료가 되면 catch 구문에 err.name = TokenExpiredError, err.message = jwt expired 가 출력된다.
  • 유효기간 만료 시 res.json으로 message를 리턴해줬다.

res를 두 번 보내면 생기는 일

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

➡️ res 응답 리턴을 두 번 보내게 돼서 생기는 에러

const addLike = (req, res)=>{
    let book_id = req.params.id;

    // 인증 복호화
    let authorization = ensureAuth(req, res);

    // 좋아요 추가
    let sql = `INSERT INTO likes (user_id, liked_book_id) VALUES (?, ?)`;
    let values = [authorization.id, book_id];
    // 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
        }
   );
}
  • ensureAuth에서 res.json()을 리턴해주면 addLike에서는 res.json()을 두번 리턴해주게 된다.
  • ensureAuth에서는 err만 리턴해준다.
// 인증 복호화
function ensureAuth(req, res){
    try {
        let receivedJwt= req.headers["authorization"];
        console.log("received jwt: ", receivedJwt);

        let decodedJwt = jwt.verify(receivedJwt, process.env.PRIVATE_KEY);
        console.log(decodedJwt);
        return decodedJwt;
    } catch (err) {
        console.log(err.name);
        console.log(err.message);

        return err;
    }
}

instanceOf 사용하기

  • instanceof 연산자는 인스턴스(객체)가 해당 클래스의 인스턴스인지를 판단하는 연산자이다.
  • ensureAuth가 리턴해준 err 안에 TokenExpired 에러가 있다면 분기 처리로 res.status.json()을 리턴할 수 있다.
  • 마찬가지로 JsonWebTokenError(잘못된 토큰 입력) 에러 발생 시 BAD_REQUEST로 에러 메세지를 리턴한다.
  • 분기 처리를 통해 return res.json()은 한 번만 해줄 수 있다.
// 인증 복호화
    let authorization = ensureAuth(req, res);

    // instanceof : 객체가 어떤 클래스의 인스턴스인지 알아내기 위해 사용
    if(authorization instanceof jwt.TokenExpiredError){
        return res.status(StatusCodes.UNAUTHORIZED).json({
            "message" : "로그인 세션이 만료되었습니다. 다시 로그인하세요."
        });
    }else if(authorization instanceof jwt.JsonWebTokenError){
        return res.status(StatusCodes.BAD_REQUEST).json({
            "message" : "잘못된 토큰입니다."
        });
    }    
    else{
        let sql = `INSERT INTO cartItems (book_id, quantity, user_id) VALUES (?, ?, ?)`;
        let values = [book_id, quantity, authorization.id];