[TIL] Node.js 숙련 (4) 23.06.22

이상훈·2023년 6월 23일
1

[내일배움캠프]

목록 보기
33/68

✔️오늘 한일!

  • refresh token을 추가하여 로그인 유지 기능 추가
  • 로그아웃 기능 추가
  • 계정전환 기능 추가

필수구현 사항을 마치고 이전에 같은 팀원이였던 분과 추가 기능에 대해서 논의해보다 로그인 유지 기능을 같이 구현해보기로 결정했다. 아직은 잘 모르는 개념이라 작업하면서도 이게 맞나 싶기도하면서 하나하나 맞춰가면서 작업하다보니 코드가 너무 길어진 느낌이 있다.

1) 먼저 Tokens migrations, modles를 생성하여 users와 1:1 관계 설정

2) 로그인 시 refresh token을 생성

// 로그인 API
router.post("/login", async (req, res) => {
  const { email, password } = req.body;

  try {
    const user = await Users.findOne({ where: { email } });
    const savedTokenUser = await Tokens.findOne({ where: { UserId: user.userId } });

    if (!user || password !== user.password) {
      return res.status(400).json({
        errorMessage: "이메일 또는 패스워드가 틀렸습니다.",
      });
    }

    if (!savedTokenUser) {
      // 리프레시 토큰 생성
      const refreshToken = jwt.sign({}, process.env.JWT_SECRET_KEY, { expiresIn: "7d" });
      // 액세스 토큰 생성
      const accessToken = jwt.sign({ userId: user.userId }, process.env.JWT_SECRET_KEY, {
        expiresIn: "30m",
      });
      await Tokens.create({ tokenId: refreshToken, UserId: user.userId });
      res.cookie("authorization", `Bearer ${accessToken}`);
      return res.status(200).json({
        accessToken,
        message: "로그인에 성공하였습니다.",
      });
    }
    try {
      // 토큰 만료검사
      jwt.verify(savedTokenUser.tokenId, process.env.JWT_SECRET_KEY);

      await Tokens.destroy({ where: { UserId: user.userId } });
      await Tokens.create({ tokenId: savedTokenUser.tokenId, UserId: user.userId });

      const accessToken = jwt.sign({ userId: user.userId }, process.env.JWT_SECRET_KEY, {
        expiresIn: "30m",
      });
      res.cookie("authorization", `Bearer ${accessToken}`);
      return res.status(200).json({
        accessToken,
        message: "로그인에 성공하였습니다.",
      });
    } catch (err) {
      if (err.name === "TokenExpiredError") {
        const refreshToken = jwt.sign({}, process.env.JWT_SECRET_KEY, { expiresIn: "7d" });
        const accessToken = jwt.sign({ userId: user.userId }, process.env.JWT_SECRET_KEY, {
          expiresIn: "30m",
        });
        await Tokens.destroy({ where: { UserId: user.userId } });
        await Tokens.create({ tokenId: refreshToken, UserId: user.userId });
        res.cookie("authorization", `Bearer ${accessToken}`);
        return res.status(200).json({
          accessToken,
          message: "로그인에 성공하였습니다.",
        });
      } else {
        return res.status(400).json({
          message: "로그인에 실패하였습니다.",
        });
      }
    }
  } catch (err) {
    return res.status(400).json({
      message: "존재하지 않는 아이디 또는 잘못된 접근입니다.",
    });
  }
});

DB를 조회하여 해당 userId로 생성된 refresh token이 없다면 새로 생성해주고, 있다면 해당 토큰의 유효기간을 검증하여 만료된 토큰이 있다면 DB에서 삭제 후 재생성해주는 구조이다.
토큰검증 middleware를 만들었다면 코드를 좀 더 가독성이 좋게 줄일 수 있을 것 같은데 과제 제출 기한이 얼마 남지 않아 일단 구현을 목표로 진행했다.

3) 사용자 검증 middleware

// 사용자 인증 미들웨어
module.exports = async (req, res, next) => {
  const { authorization } = req.cookies;
  const refreshToken = await Tokens.findOne({ order: [["createdAt", "DESC"]] });
  const [accessTokenType, accessToken] = (authorization ?? "").split(" ");

  try {
    // case 1 : accessToken과 refreshToken 둘다 없는 경우
    if (!refreshToken && !accessToken) {
      return res.status(401).send({
        message: "로그인 후 이용 가능한 기능입니다.",
      });
    }
    // case 2 : accessToken은 없고 refreshToken은 있는 경우
    else if (!accessToken && refreshToken) {
      jwt.verify(refreshToken.tokenId, process.env.JWT_SECRET_KEY);
      const accessToken = jwt.sign({ userId: refreshToken.UserId }, process.env.JWT_SECRET_KEY, {
        expiresIn: "30m",
      });
      res.cookie("authorization", `Bearer ${accessToken}`);

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

      if (!user) {
        res.clearCookie("authorization");
        return res.status(401).json({
          message: "토큰 사용자가 존재하지 않습니다.",
        });
      }

      res.locals.user = user;
      next();
    }
    // case 3 : 둘다 만료 검사
    else {
      jwt.verify(refreshToken.tokenId, process.env.JWT_SECRET_KEY);
      const decodedToken = jwt.verify(accessToken, process.env.JWT_SECRET_KEY);
      const userId = decodedToken.userId;
      const user = await Users.findOne({ where: { userId } });

      res.locals.user = user;
      next();
    }
  } catch (err) {
    if (err.name === "TokenExpiredError") {
      res.clearCookie("authorization");
      return res.status(401).send({
        errorMessage: "만료된 토큰입니다. 다시 로그인 해주세요.",
      });
    } else {
      return res.status(400).json({
        message: "잘못된 접근 방법입니다.",
      });
    }
  }
};

일단 로그인 후 엑세스 토큰이 없더라도 post 생성 시 자동으로 엑세스 토큰이 재발급이 되고 생각한대로 기능 구현이 되긴 했다.
팀원분과 구글링해보면서 뚝딱거리면서 만들다보니 검증도 중복되는 부분이 있을 수도 있고 비효율적인 접근 방법일 수도 있다. 심화과정에서 refresh token에 대한 수업이 있을지는 모르겠지만 따로 공부해보고 코드도 다시 바꿔봐야겠다.

profile
코린이

0개의 댓글