feat: JWT 토큰 자동 갱신 (슬라이딩 세션)
- 토큰 만료 시간 30일로 변경 - /auth/refresh 엔드포인트 추가 (만료 7일 전 갱신) - 프론트엔드에서 1시간마다 토큰 자동 갱신 체크
This commit is contained in:
parent
74cf074d7e
commit
c6a99ce4ce
2 changed files with 73 additions and 2 deletions
|
|
@ -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 - 로그아웃 (클라이언트에서 토큰 삭제)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue