점진적 토큰 갱신(Incremental Token Renewal)은 보안성과 사용자 경험을 고려하여 액세스 토큰(Access Token)을 갱신하는 방식이다.
기본적으로 액세스 토큰은 짧은 유효 기간, 리프레시 토큰은 긴 유효 기간을 가지며, 점진적 갱신을 통해 리프레시 토큰의 보안성을 유지하면서 인증 상태를 지속한다.
액세스 토큰을 자주 갱신하되, 리프레시 토큰 갱신은 최소화
리프레시 토큰의 롤링(순환) 방식 적용
사용자가 장기간 로그인 상태를 유지하면 리프레시 토큰도 주기적으로 갱신
사용자가 로그인하면 액세스 토큰과 리프레시 토큰을 발급한다.
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",
});
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;
}
✅ 효과: 사용자는 로그아웃 없이 지속적으로 로그인 유지 가능.
리프레시 토큰을 매번 갱신하지 않고, 특정 조건에서만 교체한다.
리프레시 토큰을 갱신하는 조건
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% 이상 사용되었으면 갱신
}
✅ 효과: 리프레시 토큰을 자주 갱신하지 않아 보안성에 도움(리프레시 토큰을 자주 갱신하면 새로운 토큰이 클라이언트와 서버 간에 빈번하게 전송됨 → 네트워크에서 탈취될 가능성이 증가.)
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" });
}
});
✅ 효과: 리프레시 토큰의 사용량을 체크하고 필요할 때만 갱신하여 보안 유지.
사용자가 로그아웃하면 리프레시 토큰을 즉시 삭제해야 한다.
app.post("/auth/logout", (req, res) => {
res.clearCookie("refreshToken");
res.json({ message: "Logged out" });
});
✅ 효과: 탈취된 리프레시 토큰을 이용한 불법 접근 차단.
POST /auth/refresh
호출하여 갱신 ✅ 장점
이 방식으로 구현하면 보안과 사용자 경험을 모두 최적화할 수 있다.