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);
|
clearLoginAttempts(ip);
|
||||||
|
|
||||||
// JWT 토큰 생성
|
// JWT 토큰 생성 (30일 유효)
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ id: user.id, email: user.email, isAdmin: user.is_admin },
|
{ id: user.id, email: user.email, isAdmin: user.is_admin },
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
{ expiresIn: "24h" }
|
{ expiresIn: "30d" }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[Auth] 로그인 성공: ${user.email} - IP: ${ip}`);
|
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 - 로그아웃 (클라이언트에서 토큰 삭제)
|
* 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) => {
|
const login = async (email, password) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue