본문 바로가기
JS/Node.js

TIL(23.06.23) refreshToken을 활용한 유저 계정 전환 기능

by 썸맨 2023. 6. 24.

★ SPA_BLOG_LV3 development!

 - 기존에 만들어 놓은 refreshToken도 괜찮지만 결국 이 Tokens의 DB에는 하나의 데이터만 저장이 되기에 DB로서의 효율성은 떨어지지 않나? 라고 같이 작업하시는 상훈님이 의견을 남겨주셨음

 - 다른 해결 방법?  그러면 DB도 좀 더 유용하게 사용이 가능하면서 refreshToken을 사용할 수 있는 방법을 고안하다가 찾은 방법이 흔히 웹사이트에서 볼 수 있는 사용자 계정 전환 기능으로 development 하기로 함.

 

 ☆ 알고리즘 구상

  • 1) 사용자가 처음 로그인을 하게 된다면 accessToken과 refreshToken을 같이 발급함, 만료기한은 이전과 동일, 이미 refreshToken을 발급받은 사용자가 재 로그인을 하게 된다면 해당 토큰을 검증 해 검증이 완료되면 새 accessToken만 발급을 하고 refreshToken은 그대로 사용을 하되 DB에 저장된 해당 userId의 토큰은 한 번 지웠다가 다시 생성함( why? 여러 사용자가 들어와 있는 구조 상 현재 로그인 중이던 사용자의 토큰이 만료되거나 로그아웃을 했을 때 남아 있는 사용자 중에 어떤 사용자가 로그인 상태를 유지할 것이냐? 라고 생각을 했었을 때, 그럼 가장 마지막으로 로그인을 했던 사용자 즉, createdAt이 가장 최신인 사용자(DESC했을 때 제일 위에 위치)를 로그인 상태로 자동으로 변경되도록 하자! 라고 생각이 들었기 때문에 로그인 순서를 정하기 위해서 지웠다가 다시 생성하는 방법을 차용)
  • 2) 로그아웃 기능을 추가해서 userId를 params로 받아 해당 userId의 토큰 값만을 DB에서 삭제하고 쿠키도 삭제를 해서 로그아웃이 되도록 설정

☆ 문제점 ?  기존에 저장된 쿠키를 삭제하게 된다면 지금 현재 로그인 된 유저의 쿠키 값을 지워버린다는 것으로 만약 내가 로그인된 유저가 아닌 다른 즉 refreshToken에만 저장이 되어 있는 사용자를 로그아웃 하고 싶을 경우에는 원치 않는 결과가 발생할 수 있지 않을 까 생각(ex) 현재 user1로 로그인이 되어있고 Tokens에는 uesr1, 2, 3 세 개의 토큰이 저장이 되어 있을 때, user2를 로그아웃 한다고 하면 uer1의 쿠키 값까지 삭제가 됨)

=> 해결 : 어차피 기존의 쿠키 값이 삭제가 되어 있더라도 로그인 되어 있는 유저 즉 user1의 정보는 DB에 저장이 되어 있고 accessToken이 없을 때 refreshToken을 검증해서 새로 발급 받으면 되기에 큰 문제가 되지 않아 보임!

  • 3) 사용자 계정 전환 기능 추가 : userId를 params로 받아 해당 userId의 토큰이 DB에 존재하고 이 토큰의 검증이 통과가 된다면 이 토큰을 현재 로그인 된 유저의 정보로 바꾸기 위해 DB에서 삭제하고 재생성, accessToken을 새로 발급

 ☆ 코드

1) 로그인

// 해당 userId의 토큰이 DB에 없는 경우
const existReFreshToken = await Tokens.findOne({ where: { UserId: user.userId } });
if (!existReFreshToken) {
      const refreshToken = jwt.sign({}, process.env.JWT_SECRET_KEY, { expiresIn: "14d" });
      const accessToken = jwt.sign({ userId: user.userId }, process.env.JWT_SECRET_KEY, {
        expiresIn: "1h",
      });

      await Tokens.create({ tokenId: refreshToken, UserId: user.userId });
      res.cookie("accessToken", `Bearer ${accessToken}`);
}
// 있다면 검증 실시해서 검증이 성공하면 DB는 삭제 후 재 생성(tokenId는 동일!)하고 새 accessToken을 생성
try {
    jwt.verify(existReFreshToken.tokenId, process.env.JWT_SECRET_KEY);
    await Tokens.destroy({ where: { UserId: user.userId } });
    await Tokens.create({ tokenId: existReFreshToken.tokenId, UserId: user.userId });

    const accessToken = jwt.sign({ userId: user.userId }, process.env.JWT_SECRET_KEY, {
      expiresIn: "1h",
    });

    res.cookie("accessToken", `Bearer ${accessToken}`);
}
catch (error) {
    // refreshToken이 만료되었을 경우
    // 두 토큰을 전부 생성
    if (error.name === "TokenExpiredError"
    => 맨 위 처럼 두 토큰을 전부 생성

=> 사용자 계정 전환 API도 로그인과 유사한 로직이기에 계정 전환 코드는 생략

 

2) 로그아웃

    const { userId } = req.params;
    const user = await Users.findOne({ where: { userId } });
    const existToken = await Tokens.findOne({ where: { UserId: userId } });

    if (!existToken) {
      return res.status(404).json({
        success: false,
        errorMessage: "로그인이 되어 있지 않은 아이디입니다.",
      });
    }

    await Tokens.destroy({ where: { UserId: userId } });
    res.clearCookie("accessToken");

=> 로그인이 되어 있는 정보인지 확인하고 DB에 저장된 정보라면 삭제하고 쿠키도 삭제(현재 로그인한 유저일 가능성이 제일 높으니 삭제)

 

 

 ☆ 해당 코드를 정상 작동시키기 위한 auth-middleware.js 수정

  • 원초적으로 로그인이 필요한 기능의 작업을 수행하기 위해 쿠키에 담긴 유저 정보(로그인 된 사용자)가 있어야 함.
  • 그렇다면 우리는 accessToken의 검증만 완료가 된다면 해당 작업을 사용이 가능 하기 때문에 accessToken의 상태가 제일 중요!(refreshToken은 accessToken이 만료되거나 삭제 되어 없을 때를 위해 사용하기 위한 토큰id)
  • 알고리즘 구상 1) accessToken과 refreshToken이 둘 다 없다면 토큰이 아예 없는 경우니까 로그인을 하도록 설정
  • 2) accessToken이 삭제(만료x)된 경우 refreshToken을 검증 해 검증이 완료되면 해당 userId의 accessToken을 생성
  • 3) accessToken과 refreshToken이 둘다 있을 때 accessToken을 검증해 검증이 되면 바로 해당 값을 로그인된 유저의 정보로 사용하고 만약 만료가 되었다면 그 다음에 refreshToken을 검증을 해서 이 refreshToken이 검증이 되면 새로운 accessToken을 발급하고 만료가 된 refreshToken이라면 DB에서 해당 토큰을 삭제하고 재로그인 하라는 오류 반환
  • why refreshToken 만료시 삭제 ? 해당 토큰이 만료가 되어 있다면 자연스럽게 그 다음으로 로그인을 한 사용자가 로그인이 되는 상황으로 바꾸기 위해서 ex) 만약 여러 사용자가 로그인 된 상황에서 내가 로그아웃을 하거나 쿠키가 삭제가 되어서 만료된 refreshToken을 검증해야 하는 상황일 때, 로그아웃하거나 잘못된 토큰을 가진 사용자의 다음 사용자의 정보로 넘어가기 위해서는 삭제를 해줘야 함(로그인 된 유저의 정보를 가져올 때 생성된 역순의 제일 처음 것만 가져와 해당 유저를 현재 로그인 된 유저로 인식하도록 구현했기 때문) - 삭제해 주지 않으면 다음으로 넘어가지 않음

 

☆ 코드

 - 구상 1, 2는 이전에 작성한 코드와 일치하기에 생략

 case 3)

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

        if (!user) {
          res.clearCookie("accessToken");
          return res
            .status(403)
            .json({ success: false, errorMessage: "토큰 사용자가 존재하지 않습니다." });
        }

        res.locals.user = user;
        res.locals.userNickname = user.nickname;

        next();

=> 둘 다 있다면 accessToken을 검증해서 검증이 되면 해당 토큰이 현재 로그인 된 유저의 정보

 

case 4)

catch (error) {
        // case 4) 토큰이 둘 다 있는데 accessToken만 만료된 경우
        // 이 때, refreshToken을 검증해 만료가 되지 않았다면 새 accessToken을 발급하고 만료 되었다면
        // 만료된 refresh토큰을 삭제하고 다시 로그인 하도록 설정함.
        if (error.name === "TokenExpiredError") {
          jwt.verify(existReFreshToken.tokenId, process.env.JWT_SECRET_KEY);

          const accessToken = jwt.sign(
            { userId: existReFreshToken.UserId },
            process.env.JWT_SECRET_KEY,
            {
              expiresIn: "1h",
            }
          );
          res.cookie("accessToken", `Bearer ${accessToken}`);

=> accessToken이 만료가 되었다면 refreshToken을 검증 해 검증이 되면 새 accessToken을 생성

 

case 5) accessToken 과 refreshToken 둘다 만료

catch (error) {
    // accessToken과 refreshToken 모두 만료된 경우
    // 여러 계정을 저장하기 때문에 가장 최근에 로그인 한 순서대로 비교를 해 만료되지 않은 사용자에게
    // 등록/수정/삭제 권한을 주기 위해서 refreshToken이 만료된 유저는 토큰이 삭제가 되도록 구현
    if (error.name === "TokenExpiredError") {
      if (existReFreshToken)
        await Tokens.destroy({ where: { tokenId: existReFreshToken.tokenId } });

      res.status(403).json({
        success: false,
        message: "토큰이 만료된 아이디입니다. 다시 로그인 해주세요.",
      });

=> 둘 다 만료가 되었다면 해당 tokenId를 DB에서 삭제하고 만료 되었으니 다시 로그인 해달라고 메시지 반환

 

 

☆최종 완료된 코드의 DB의 ERD

 

 

※ 간단하게 추가해야 하지라고 생각해서 그렇게 길게 걸리지 않을 꺼같다 생각했었는데 상훈님과 서로 구현하려 했던 방식이 다르기도 했고 체크해야 할 상황이 구현하면서 많이 생겨서 좀 오래 걸렸다. 다음에 진행을 할 때는 필요한 요구사항이나 기능을 확실하게 정리하고 development를 하면 될 거 같다!

'JS > Node.js' 카테고리의 다른 글

TIL(2023.06.27)  (0) 2023.06.29
Node.js 숙련  (0) 2023.06.25
TIL(23.06.22) .env 설정 / refreshToken 생성  (0) 2023.06.24
TIL(23.06.21) - 게시글 / 댓글 RestAPI 구현  (0) 2023.06.22
Node.js 입문  (0) 2023.06.20