feat: JWT 토큰 자동 갱신 (슬라이딩 세션)

- 토큰 만료 시간 30일로 변경
- /auth/refresh 엔드포인트 추가 (만료 7일 전 갱신)
- 프론트엔드에서 1시간마다 토큰 자동 갱신 체크
This commit is contained in:
caadiq 2025-12-24 09:59:05 +09:00
parent 74cf074d7e
commit c6a99ce4ce
2 changed files with 73 additions and 2 deletions

View file

@ -290,11 +290,11 @@ router.post("/login", async (req, res) => {
// 로그인 성공
clearLoginAttempts(ip);
// JWT 토큰 생성
// JWT 토큰 생성 (30일 유효)
const token = jwt.sign(
{ id: user.id, email: user.email, isAdmin: user.is_admin },
JWT_SECRET,
{ expiresIn: "24h" }
{ expiresIn: "30d" }
);
console.log(`[Auth] 로그인 성공: ${user.email} - IP: ${ip}`);
@ -356,6 +356,44 @@ router.get("/me", async (req, res) => {
}
});
/**
* POST /auth/refresh - 토큰 갱신 (슬라이딩 세션)
* 만료 7 전이면 토큰 발급
*/
router.post("/refresh", async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "토큰이 없습니다." });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
// 만료까지 남은 시간 계산 (초)
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
const sevenDaysInSeconds = 7 * 24 * 60 * 60;
// 만료 7일 전이면 새 토큰 발급
if (expiresIn < sevenDaysInSeconds) {
const newToken = jwt.sign(
{ id: decoded.id, email: decoded.email, isAdmin: decoded.isAdmin },
JWT_SECRET,
{ expiresIn: "30d" }
);
console.log(`[Auth] 토큰 갱신: ${decoded.email}`);
return res.json({ refreshed: true, token: newToken });
}
// 아직 갱신 불필요
res.json({ refreshed: false });
} catch (error) {
// 토큰이 만료된 경우
return res.status(401).json({ error: "토큰이 만료되었습니다." });
}
});
/**
* POST /auth/logout - 로그아웃 (클라이언트에서 토큰 삭제)
*/

View file

@ -45,6 +45,39 @@ export function AuthProvider({ children }) {
}
};
// ( )
const refreshToken = async () => {
const token = localStorage.getItem('token');
if (!token) return;
try {
const response = await fetch('/auth/refresh', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.refreshed && data.token) {
localStorage.setItem('token', data.token);
console.log('[Auth] 토큰 자동 갱신됨');
}
} catch (error) {
// ( )
}
};
// 1
useEffect(() => {
if (!user) return;
//
refreshToken();
// 1
const interval = setInterval(refreshToken, 60 * 60 * 1000);
return () => clearInterval(interval);
}, [user]);
//
const login = async (email, password) => {
try {