JWT 활용하기

해준박·2024년 1월 9일
0
post-custom-banner

JWT 토큰 인증 이란?

JWT과 무엇인지와 사용방법을 익히고 왜 프로젝트에 적용을 해야 하는지 고민해보았다.

  1. 세션저장소를 사용 할 수록 서버의 부하가 심해 질 수 있다.

    • 서버의 메모리가 제한적이고 내가 사용하고 있는 서버는 aws ec2의 프리티어의 소규모 서버이기 때문에 세션 데이터가 커지거나 혹시나 서버에 문제가 생긴다면 세션데이터에 문제가 생길 수 있다. 물론 DB에 저장하는 방법이 있지만 부하가 일어 날 수도있으며, 비용적인 문제가 커질 가능성이 높다.
  2. JWT의 state-less(무상태성)

    • 위의 내용처럼 세션은 서버에서 사용자의 세션을 저장하고 유지한다. 반면, JWT는 클라이언트에서 관리함으로 서버는 JWT의 유효성 검사만 하면된다. 결론적으로 서버에서 사용자의 상태를 관리하지 않게되며 서버쪽에서 요청을 좀 더 처리할 수 있고, 확장성이 향상된다.

새로운 기술을 알게되어 적용을 해보고싶어서 그냥 적용하기보다는 왜 적용해야하는가에 좀 더 생각해보았다. 물론 단점도 있지만 그 부분을 보완해가면서 진행하면 될 것 같다.

대표적으로 토큰은 노출되어있다. 쿠키나 로컬에 저장을 하게 될텐데, 이는 클라이언트에서 누구나 볼 수 있다는 거다. 여기서 액세스토큰, 리프레쉬토큰 둘 다 저장을 하되 액세스토큰의 유효기간을 매우 짧게 설정 하고, 리프레쉬토큰을 좀 더 안전하게 저장하는 방법을 생각해보면 될 것 같다.

workflow

  1. 로그인 시, 액세스토큰 및 리프레쉬 토큰 발급
  2. API 요청 시 (정보수정, 상품 업로드 등) 서버에서 액세스 토큰 유효성 검사
  3. 토큰이 유효할 경우 응답
  4. 만약, 액세스 토큰이 유효하지 않을 경우, 리프레쉬토큰의 유효성 검사
  5. 리프레쉬 토큰이 유효 할 경우 액세스토큰을 발급 후 응답
  6. 리프레쉬 토큰이 유효하지 않을 경우 재로그인 요청

내가 생각한 순서다 아마 토큰의 유효성을 검사하는것을 서버에서 미들웨어로 설정하면 되지 않을까?

  • 토큰이 계속 안넘어 오길래 왜 안넘어오지 하니 여러가지 설정 문제가 있었다. 해결방법은 프록시 설정을 했음

참고사이트


1. 로그인시 토큰 발급

내 프로젝트에는 접근성을 위해 카카오로그인만을 사용하고있다. (네이버로그인도 있지만 현재 안쓰고 있고 JWT를 적용 후 다룰 예정)

먼저, 클라이언트와 카카오 사이의 로그인을 진행 후, 완료되었다면 토큰을 발급되게 해주었다.

// root.js
const result = await kakaoAuth.getProfile(access_token);

      const user = result;
      kakaoUser = {
        social_id: { value: user.id, social_name: "카카오 로그인" },
        email: user.kakao_account.email,
        nickname: user.kakao_account.profile.nickname,
        image: user.kakao_account.profile.profile_image_url,
      };

      const isUser = await User.findOne({ email: kakaoUser.email })
        .populate({
          path: "on_sale",
          populate: { path: "seller_info" },
        })
        .populate({ path: "chat_rooms", populate: { path: "message_log", populate: { path: "send_user" } } })
        .populate({ path: "chat_rooms", populate: { path: "member_list" } })
        .populate({ path: "chat_rooms", populate: { path: "product" } });

      if (!isUser) {
        sendUser = new User(kakaoUser);
        await sendUser.save();
      } else {
        sendUser = isUser;
      }

      responseData = { success: true, user: sendUser };

      if (access_token) {
        // 액세스 토큰
        const accessToken = jwt.sign(
          {
            email: sendUser.email,
            social_id: { ...sendUser.social_id },
            issuer: "ikw-market",
          },
          process.env.JWT_SECRET_KEY,
          { expiresIn: "1m" }
        );
        // 리프레쉬 토큰
        const refreshToken = jwt.sign(
          {
            email: sendUser.email,
            social_id: { ...sendUser.social_id },
            issuer: "ikw-market",
          },
          process.env.JWT_SECRET_KEY,
          { expiresIn: "24h" }
        );

        res.cookie("accessToken", accessToken, {
          secure: true,
          sameSite: "none",
        });
        res.cookie("refreshToken", refreshToken, {
          secure: true,
          sameSite: "none",
        });
      }
      return res.status(200).json(responseData);

payload에는 중요한 정보를 담기엔 위험해서 이메일, 필요한정보, 발행자 정도로 넣어줬다. 그러고 응답으로 쿠키에 토큰을 담아 보내게 했음
expiresIn 설정으로 액세스토큰은 1분 리프레쉬토큰은 24시간으로 각각 설정 해두었다.

  • 근데 액세스,리프레쉬 토큰을 둘다 똑같은데 왜 리프레쉬 토큰으로 액세스토큰을 또 발급하는 방식을 쓰는 걸까 어차피 리프레쉬 토큰이 털리면 액세스 토큰도 다시 발급 받을 수 있는거 아닌가?
    내가 생각한 결론은, 리프레시는 액세스토큰을 발급 받기 위한 토큰이다 그래서 여기에는 이메일 외에는 다른 정보를 넣지 않고 액세스토큰을 발급 받기 위한 토큰 역할만 하는 걸로 했다

2. API 요청시 토큰 검사

export const tokenCheck = (req, res, next) => {
  const accessToken = req.cookies.accessToken;
  const refreshToken = req.cookies.refreshToken;

  if (!accessToken) return res.status(401).json({ error: "Unauthorized" });
  if (!refreshToken) return res.status(401).json({ error: "Unauthorized" });

  // 액세스 토큰 체크하는 함수
  const accessTokenCheck = async () => {
    try {
      jwt.verify(accessToken, process.env.JWT_SECRET_KEY);
      return next();
    } catch (error) {
      // 액세스 토큰 체크 후 리프레시 토큰 체크
      await refreshTokenCheck();
    }
  };

  // 리프레시 토큰 체크하는 함수
  const refreshTokenCheck = async () => {
    try {
      // 유효한지 검사
      const payload = await jwt.verify(refreshToken, process.env.JWT_SECRET_KEY);

      const accessToken = createAccesToken(payload.email);

      res.cookie("accessToken", accessToken, {
        secure: true,
        sameSite: "none",
      });
      return next();
    } catch (error) {
      console.error("리프레시 토큰 검사 실패:", error);

      return res.status(401).json({ error: "Unauthorized" });
    }
  };
  accessTokenCheck();
};

// 액세스토큰 생성
export const createAccesToken = async (email) => {
  const user = await User.findOne({ email })
    .populate({
      path: "on_sale",
      populate: { path: "seller_info" },
    })
    .populate({ path: "chat_rooms", populate: { path: "message_log", populate: { path: "send_user" } } })
    .populate({ path: "chat_rooms", populate: { path: "member_list" } })
    .populate({ path: "chat_rooms", populate: { path: "product" } });

  if (!user) {
    return res.status(400).json({ err: "찾을수 없는 사용자" });
  }

  const accessToken = await jwt.sign(
    {
      email: user.email,
      social_id: { ...user.social_id },
      issuer: "ikw-market",
    },
    process.env.JWT_SECRET_KEY,
    { expiresIn: "1m" }
  );
  return accessToken;
};

api 요청이 왔을때 토큰을 검사할 미들웨어를 하나 만들고, 토큰들의 유무, 유효성을 검사 후 재발급 또는 로그인 페이지로 이동하게 처리했다 처음에는 res.redirect를 통해 클라이언트 페이지를 /login 페이지로 이동하게 하려는 멍청한 짓을 했다. 이 부분은 응답으로 status401을 보내고 클라이언트 단에서 401이 뜨면 로그인 페이지로 이동하게 했다.


하고 보니 정말 간단한 작업이였던거 같다 조금 보완해야할 점은 리프레쉬토큰을 좀 더 안전하게 보관하는법과 프로덕션 단계에서의 쿠키 굽기이다 로컬에서는 잘 되지만 배포 시 좀 달라질거 같아서 추후 배포 할 때 다시 작업 해야 할 듯

profile
기록하기
post-custom-banner

0개의 댓글