JWT (JSON Web Token) - 점진적 토큰 갱신

FeelsBotMan·2025년 3월 7일
1

JWT

목록 보기
4/5
post-thumbnail

점진적 토큰 갱신(Incremental Token Renewal)은 보안성과 사용자 경험을 고려하여 액세스 토큰(Access Token)을 갱신하는 방식이다.
기본적으로 액세스 토큰은 짧은 유효 기간, 리프레시 토큰은 긴 유효 기간을 가지며, 점진적 갱신을 통해 리프레시 토큰의 보안성을 유지하면서 인증 상태를 지속한다.


점진적 토큰 갱신의 핵심 원칙

  1. 액세스 토큰을 자주 갱신하되, 리프레시 토큰 갱신은 최소화

    • 액세스 토큰이 만료될 때마다 새로운 액세스 토큰을 발급.
    • 리프레시 토큰은 일정 조건에서만 갱신하여 탈취 가능성을 줄임.
  2. 리프레시 토큰의 롤링(순환) 방식 적용

    • 리프레시 토큰을 사용할 때마다 새로운 리프레시 토큰을 발급하고, 기존 토큰을 폐기.
    • 이를 통해 탈취된 리프레시 토큰의 재사용 공격을 방지.
  3. 사용자가 장기간 로그인 상태를 유지하면 리프레시 토큰도 주기적으로 갱신

    • 일정 기간 동안 리프레시 토큰이 사용되지 않으면 폐기.

점진적 토큰 갱신 구현 방식

1. 기본 토큰 발급

사용자가 로그인하면 액세스 토큰과 리프레시 토큰을 발급한다.

const accessToken = jwt.sign({ userId }, process.env.ACCESS_SECRET, {
  expiresIn: "15m", // 짧은 유효기간
});

const refreshToken = jwt.sign({ userId }, process.env.REFRESH_SECRET, {
  expiresIn: "7d", // 긴 유효기간
});

// 액세스 토큰은 클라이언트가 저장 (ex: 메모리, 상태관리)
// 리프레시 토큰은 쿠키에 저장 (HttpOnly, Secure 설정)
res.cookie("refreshToken", refreshToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: "Strict",
});

2. 액세스 토큰 자동 갱신 (프론트엔드)

  • 액세스 토큰이 만료되기 약 1~2분 전에 새로운 액세스 토큰을 요청.
  • refreshToken을 이용하여 POST /auth/refresh API를 호출.
async function refreshAccessToken() {
  const response = await fetch("/auth/refresh", { method: "POST", credentials: "include" });
  if (response.ok) {
    const data = await response.json();
    return data.accessToken;
  }
  return null;
}

효과: 사용자는 로그아웃 없이 지속적으로 로그인 유지 가능.


3. 리프레시 토큰 갱신 조건

리프레시 토큰을 매번 갱신하지 않고, 특정 조건에서만 교체한다.

리프레시 토큰을 갱신하는 조건

  • ① 액세스 토큰을 갱신할 때, 리프레시 토큰이 30% 이상의 유효 기간을 사용한 경우
    예) 7일짜리 토큰이라면 2일 이상 사용되었을 때만 새 리프레시 토큰 발급
  • ② 로그인 유지 시간이 X일 이상 경과한 경우
function shouldRefreshToken(iat, exp) {
  const now = Math.floor(Date.now() / 1000);
  const usedTime = now - iat;
  const totalLifetime = exp - iat;

  return usedTime / totalLifetime > 0.3; // 30% 이상 사용되었으면 갱신
}

효과: 리프레시 토큰을 자주 갱신하지 않아 보안성에 도움(리프레시 토큰을 자주 갱신하면 새로운 토큰이 클라이언트와 서버 간에 빈번하게 전송됨 → 네트워크에서 탈취될 가능성이 증가.)


4. 리프레시 토큰을 사용한 액세스 토큰 갱신 API

app.post("/auth/refresh", async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.status(401).json({ message: "Unauthorized" });

  try {
    const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    const newAccessToken = jwt.sign({ userId: payload.userId }, process.env.ACCESS_SECRET, { expiresIn: "15m" });

    // 리프레시 토큰이 30% 이상 사용되었으면 새로 발급
    if (shouldRefreshToken(payload.iat, payload.exp)) {
      const newRefreshToken = jwt.sign({ userId: payload.userId }, process.env.REFRESH_SECRET, { expiresIn: "7d" });

      res.cookie("refreshToken", newRefreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "Strict",
      });
    }

    res.json({ accessToken: newAccessToken });
  } catch (err) {
    return res.status(403).json({ message: "Invalid refresh token" });
  }
});

효과: 리프레시 토큰의 사용량을 체크하고 필요할 때만 갱신하여 보안 유지.


5. 로그아웃 및 리프레시 토큰 무효화

사용자가 로그아웃하면 리프레시 토큰을 즉시 삭제해야 한다.

app.post("/auth/logout", (req, res) => {
  res.clearCookie("refreshToken");
  res.json({ message: "Logged out" });
});

효과: 탈취된 리프레시 토큰을 이용한 불법 접근 차단.


정리: 점진적 토큰 갱신의 흐름

  1. 로그인 시 액세스 토큰(15분)과 리프레시 토큰(7일) 발급
  2. 액세스 토큰 만료 전에 POST /auth/refresh 호출하여 갱신
  3. 리프레시 토큰이 30% 이상 사용되었을 때만 갱신
  4. 새로운 리프레시 토큰 발급 시 기존 토큰은 폐기
  5. 로그아웃 시 리프레시 토큰 삭제

장점

  • 사용자 경험 개선: 로그아웃 없이 인증 유지 가능
  • 보안 강화: 리프레시 토큰의 수명을 연장하면서도 과도한 재사용 방지
  • 서버 부하 최소화: 리프레시 토큰을 자주 갱신하지 않아 데이터베이스 부하 감소

이 방식으로 구현하면 보안과 사용자 경험을 모두 최적화할 수 있다.

profile
안드로이드 페페

0개의 댓글