본문 바로가기

프로젝트 진행기

끝판왕의 프로젝트_Node.js CRUD 실습(4)_미들웨어 구축

우리는 많은 서비스들에 회원가입을 하고 로그인하여 해당 서비스를 이용한다.

로그인이 회원이라는 것을 인증했기 때문이다. 

 

회원이라는 것을 인증하는 미들웨어를 구축하는 단계까지 왔다 ! 

#구현하고자 하는 것
- 로그인 이후 발행된 JWT토큰을 가지고 사용자 인증하기 

* 유효성 검사
- Authorization에 담겨 있는 형태가 표준과 일치 하지 않는 경우 에러 메시지
- JWT의 유효기간이 지난 경우 
- JWT 검증에 실패한 경우

 

# 로그인 시 발행된 토큰 확인하기  ! 

로그인 시 발행한 토큰 !

JWT토큰 발행 시 payload에 userId라는 키를 만들고 값에 id값을 할당했다. 

Secret키는 env파일로 관리하였고, 옵션으로 토큰의 유효기간을 12시간으로 설정하였다. 

 

요렇게 만들어진 토큰을 res.cookies(쿠키)에 담아서 응답하게 되는데 ! 

응답 시 이름은 Authorization으로 `Bearer ${토큰 값}` 을 담아 전달한다 !

 

우리는 이렇게 토큰에 담겨진 정보를 가지고 미들웨어를 구축한다 !! 

 

# 발행된 토큰 가져오기 !

const authenticate = async (req, res, next) => {
  try {
    const { Authorization } = req.cookies;
    const [tokenType, token] = (Authorization ?? "").split(" ");

    const decodedToken = jwt.verify(token, process.env.SECRET_KEY);
    const id = decodedToken.userId;
    const user = await Users.findOne({ where: { id } });

 

1) 기존 라우터와는 다르게 next 라는 매개변수 추가 

출처 : 내배캠 강의자료

next 인자는 다음 미들웨어를 호출하는 녀석이다. 

(참고로 미들웨어는 첫번째 미들웨어부터 순차적으로 진입한다.) 

 

2) 쿠키에 있는 정보를 가져와서 tokenType과 token을 분리

로그인 시 이름이 Authorization 이고 `Bearer ${토큰 값}`을 담은 쿠키를 전달했었는데 이때 Bearer가 토큰 타입이 된다.  

이 때 가져온 쿠키에 정보가 없을 경우 TypeError가 뜨는 것을 방지하기 위해 널 병합 연산자를 사용하였다. 

 

3) Secret 키로 토큰을 디코드하여 Payload에 있는 id 값을 확인하고 유저 찾기 !

토큰을 생성할 때 env파일로 관리한 Secret키를 가지고 id값을 확인하고,

DB에 Id값을 기준으로 기존 회원을 찾는다.

 

# 가져온 토큰을 가지고 유효성 검사 실행 

1) user가 존재하지 않을 경우 ! 

// 토큰 사용자가 존재하지 않을 경우
if (!user) {
  res.clearCookie("Authorization");
  return res.status(401).json({ errorMessage: "사용자가 존재하지 않습니다." });
};

 

에러 코드는 401 (Unauthorized) 를 반환하고 있다. 

토큰 값에 실려있는 id를 가지고 DB와 대조했을 때 발생하는 에러인데 사실 해당 에러가 발생하는 경우는 많이 없지 않을까 싶다. 

(로그인 후 회원 정보가 삭제되었을 때? => 아마 회원탈퇴가 아닐까 싶은데..)

 

2)  토큰 타입이 일치하지 않을 때 

// 토큰 타입이 불일치 할 경우 (Bearer 가 아닐경우)
if (tokenType !== "Bearer") {
  return res.status(401).json({ errorMessage: "토큰 타입이 일치하지 않습니다." });
};

앞서 토큰을 발행할 때 Bearer를 사용했다.

또한 쿠키에 담긴 정보를 가져올 때 토큰 타입과 값을 나눴기 때문에 위와 같이 유효성 검사를 실시했다.

 

3) 토큰 만료기간이 지났을 때 

아마 가장 흔하게 발생될 수 있는 에러로 보이는데 여기에서 몇가지 문제가 있었다 !

문제점 1.
시간으로 유효성 검사를 하는 방법을 몰랐다.

해결

검색과 챗 GPT를 활용해보니 아래와 같은 방법을 추천 !
 // 만료 여부 확인
if (decodedToken && decodedToken.exp && Date.now() / 1000 > decodedToken.exp) {
  return res.status(401).json({ errorMessage: "토큰이 만료되었습니다." });
}​


.exp라는 메서드를 활용하여 현재의 시간과 비교하는 것으로 이해했다. 
(Date.now() / 1000 는 현재 시간을 초 단위로 변환하는 부분이라고 한다.) 

 

문제점2. 
여전히 내가 원하는 에러는 잡히지 않고 catch에서 에러가 잡혔다. 

해결
역시나 검색과 GPT를 열심히 검색한 결과 ! 
에러 메시지를 이용한 유효성 검사 방법이 있었다. 
유효기간이 만료되었을 경우의 콘솔창 에러 메시지 !
에러 메시지 상단에 TokenExpiredError가 보이는가 !? 
jsonwebtoken에서는 유효기간이 만료되었을 때에 대한 별도의 에러가 지정되어 있었다 !
이것을 활용해서 아래와 같이 코드를 만들어 보았다. 
// try 문에서 토큰 만료기간 유효성 검사가 계속 안되어서.. 에러 이름으로 catch에서 실행함
if (error.name === 'TokenExpiredError') {
  return res.status(401).json({ errorMessage: "토큰이 만료되었습니다." })
};​

그 결과 내가 원했던 에러 코드와 메시지를 던질 수 있었다 !! 
여기서 생각이 들었던 것은 다른 유효성 검사도 이런식으로 해볼 수 있지 않을까 생각이 들었다. 

 

# 유효성 검사 통과 후 res.locals.user에 유저 정보 담기 !

const authenticate = async (req, res, next) => {
  try {
    const { Authorization } = req.cookies;
    const [tokenType, token] = (Authorization ?? "").split(" ");

    const decodedToken = jwt.verify(token, process.env.SECRET_KEY);
    const id = decodedToken.userId;
    const user = await Users.findOne({ where: { id } });

    // 토큰 사용자가 존재하지 않을 경우
    if (!user) {
      res.clearCookie("Authorization");
      return res.status(401).json({ errorMessage: "사용자가 존재하지 않습니다." });
    };

    // 토큰 타입이 불일치 할 경우 (Bearer 가 아닐경우)
    if (tokenType !== "Bearer") {
      return res.status(403).json({ errorMessage: "토큰 타입이 일치하지 않습니다." });
    };

    res.locals.user = user;
    next();

  } catch (error) {
    console.error(error);
    res.clearCookie("Authorization");

    // try 문에서 토큰 만료기간 유효성 검사가 계속 안되어서.. 에러 이름으로 catch에서 실행함
    if (error.name === 'TokenExpiredError') {
      return res.status(403).json({ errorMessage: "토큰이 만료되었습니다." })
    };

    return res.status(401).json({ errorMessage: "로그인이 필요합니다." });
  };
}

export default authenticate;

 

인증이 통과하게 되면 user의 정보를 로컬 환경에 저장하여 불필요한 로그인을 하지 않게 하였다. 

 

따라서 이후에 진행하는 라우터들도 유저 정보를 불러올 때는 res.locals.user에서 유저 정보를 가져올 수 있다.

(로그인이 된 상태라면) 

 

마지막으로 해당 미들웨어를 다른 파일에서도 사용하기 위하여 export를 해주었다. 

 

다음에는 인증된 사용자가 상품을 등록, 조회, 수정, 삭제 할 수 있는 API를 만들어 보도록 하겠다.