-
Notifications
You must be signed in to change notification settings - Fork 1
✨ feat: 토큰만료시 재발급 #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
✨ feat: 토큰만료시 재발급 #247
Conversation
📝 WalkthroughWalkthrough
Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant fetcher (Axios)
participant refreshFetcher (Axios)
participant localStorage
participant API
Client->>fetcher: API 요청 (access token 포함)
fetcher->>API: 요청 전달
API-->>fetcher: 401 Unauthorized (만료된 토큰)
fetcher->>localStorage: refresh token 조회
fetcher->>refreshFetcher: /auth/refresh 요청 (refresh token)
refreshFetcher->>API: 토큰 갱신 요청
API-->>refreshFetcher: 새 access/refresh token 반환
refreshFetcher-->>fetcher: 새 토큰 전달
fetcher->>localStorage: 새 토큰 저장
fetcher->>API: 원래 요청을 새 access token으로 재시도
API-->>fetcher: 정상 응답
fetcher-->>Client: 응답 반환
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~15 minutes Possibly related issues
Possibly related PRs
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
|
🧷 배포 미리보기: https://global-nomad-n01zmezg9-yun-jinwoos-projects.vercel.app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/libs/api.ts (3)
16-22: 함수 스타일 일관성 개선 제안두 토큰 가져오기 함수가 동일한 기능을 수행하지만 다른 스타일로 작성되어 있습니다. 일관성을 위해 통일하는 것이 좋겠습니다.
-const getAccessToken = (): string | null => { - if (typeof window === 'undefined') return null; - return localStorage.getItem('accessToken'); -}; +const getAccessToken = (): string | null => + typeof window === 'undefined' ? null : localStorage.getItem('accessToken'); const getRefreshToken = (): string | null => typeof window === 'undefined' ? null : localStorage.getItem('refreshToken');
54-58: 리프레시 요청에 대한 타임아웃 재검토 필요토큰 갱신은 중요한 작업이므로 3초 타임아웃이 너무 짧을 수 있습니다. 또한 공통 설정을 상수로 추출하면 유지보수가 쉬워집니다.
+const API_CONFIG = { + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + headers: { 'Content-Type': 'application/json' }, +}; + const fetcher: AxiosInstance = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + ...API_CONFIG, timeout: 3000, - headers: { - 'Content-Type': 'application/json', - }, }); const refreshFetcher = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, - timeout: 3000, - headers: { 'Content-Type': 'application/json' }, + ...API_CONFIG, + timeout: 5000, // 리프레시는 더 긴 타임아웃 허용 });
16-22: 보안 강화를 위한 토큰 저장 방식 개선 권장현재 localStorage를 사용한 토큰 저장은 XSS 공격에 취약합니다. 보안 강화를 위해 다음을 고려하세요:
- httpOnly 쿠키 사용: 리프레시 토큰은 httpOnly 쿠키로 저장하여 JavaScript 접근 차단
- 메모리 저장: 액세스 토큰은 메모리에만 저장하고 페이지 새로고침 시 리프레시로 재발급
- 토큰 암호화: localStorage 사용이 불가피하다면 최소한 암호화 적용
Also applies to: 83-84
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/libs/api.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: two678
PR: Act-It-FE/global-nomad#122
File: src/hooks/useErrorHandler.ts:1-9
Timestamp: 2025-07-27T07:24:43.556Z
Learning: axios.isAxiosError()를 사용하여 Axios 에러를 타입 안전하게 체크하는 것이 권장되는 방법입니다. 이는 error.response?.data?.message 같은 속성에 안전하게 접근할 수 있게 해줍니다.
Learnt from: Moon-ju-young
PR: Act-It-FE/global-nomad#103
File: src/libs/MyActivities.ts:28-29
Timestamp: 2025-07-25T21:18:02.241Z
Learning: axios 에러 객체에서 HTTP 상태 코드에 접근할 때는 error.status가 아닌 error.response?.status를 사용해야 합니다. error.status는 axios의 표준 속성이 아니며 일반적으로 undefined입니다.
📚 Learning: when axios instance has a global default 'content-type': 'application/json' header, formdata uploads...
Learnt from: Moon-ju-young
PR: Act-It-FE/global-nomad#197
File: src/api/activitiesApi.ts:79-83
Timestamp: 2025-08-01T18:24:05.811Z
Learning: When axios instance has a global default 'Content-Type': 'application/json' header, FormData uploads require explicit 'Content-Type': 'multipart/form-data' header override to prevent 400 Bad Request errors. The automatic Content-Type setting by axios only works when no default Content-Type is configured globally.
Applied to files:
src/libs/api.ts
📚 Learning: axios.isaxioserror()를 사용하여 axios 에러를 타입 안전하게 체크하는 것이 권장되는 방법입니다. 이는 error.response?.data?.message 같은...
Learnt from: two678
PR: Act-It-FE/global-nomad#122
File: src/hooks/useErrorHandler.ts:1-9
Timestamp: 2025-07-27T07:24:43.556Z
Learning: axios.isAxiosError()를 사용하여 Axios 에러를 타입 안전하게 체크하는 것이 권장되는 방법입니다. 이는 error.response?.data?.message 같은 속성에 안전하게 접근할 수 있게 해줍니다.
Applied to files:
src/libs/api.ts
📚 Learning: axios 에러 객체에서 http 상태 코드에 접근할 때는 error.status가 아닌 error.response?.status를 사용해야 합니다. error.status는 ax...
Learnt from: Moon-ju-young
PR: Act-It-FE/global-nomad#103
File: src/libs/MyActivities.ts:28-29
Timestamp: 2025-07-25T21:18:02.241Z
Learning: axios 에러 객체에서 HTTP 상태 코드에 접근할 때는 error.status가 아닌 error.response?.status를 사용해야 합니다. error.status는 axios의 표준 속성이 아니며 일반적으로 undefined입니다.
Applied to files:
src/libs/api.ts
| try { | ||
| const refreshToken = getRefreshToken(); | ||
| if (!refreshToken) throw new Error('No refresh token available'); | ||
| window.alert('세션이 만료되었습니다. 다시 로그인해주세요.'); | ||
|
|
||
| const { data } = await refreshFetcher.post<{ | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| }>('/auth/refresh', { refreshToken }); | ||
|
|
||
| localStorage.setItem('accessToken', data.accessToken); | ||
| localStorage.setItem('refreshToken', data.refreshToken); | ||
|
|
||
| if (originalReq.headers) { | ||
| const headers = originalReq.headers as AxiosRequestHeaders; | ||
| headers['Authorization'] = `Bearer ${data.accessToken}`; | ||
| } | ||
| return fetcher(originalReq); | ||
| } catch (refreshError) { | ||
| console.warn('토큰 갱신 실패:', refreshError); | ||
| window.alert('실패했습니다.'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토큰 갱신 로직의 치명적인 문제점들
현재 구현에 여러 심각한 문제가 있습니다:
- 리프레시 시도 전에 세션 만료 알림이 표시되어 사용자 혼란 야기
- localStorage 직접 접근 시 브라우저 체크 누락 (SSR 환경 오류 가능)
- 리프레시 실패 시 에러를 throw하지 않아 원본 요청이 재시도됨
- 모호한 실패 메시지
try {
const refreshToken = getRefreshToken();
if (!refreshToken) throw new Error('No refresh token available');
- window.alert('세션이 만료되었습니다. 다시 로그인해주세요.');
const { data } = await refreshFetcher.post<{
accessToken: string;
refreshToken: string;
}>('/auth/refresh', { refreshToken });
- localStorage.setItem('accessToken', data.accessToken);
- localStorage.setItem('refreshToken', data.refreshToken);
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('accessToken', data.accessToken);
+ localStorage.setItem('refreshToken', data.refreshToken);
+ }
if (originalReq.headers) {
const headers = originalReq.headers as AxiosRequestHeaders;
headers['Authorization'] = `Bearer ${data.accessToken}`;
}
return fetcher(originalReq);
} catch (refreshError) {
console.warn('토큰 갱신 실패:', refreshError);
- window.alert('실패했습니다.');
+ if (typeof window !== 'undefined') {
+ window.alert('세션이 만료되었습니다. 다시 로그인해주세요.');
+ }
+ // 로그인 페이지로 리다이렉트 또는 로그아웃 처리
+ throw refreshError;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const refreshToken = getRefreshToken(); | |
| if (!refreshToken) throw new Error('No refresh token available'); | |
| window.alert('세션이 만료되었습니다. 다시 로그인해주세요.'); | |
| const { data } = await refreshFetcher.post<{ | |
| accessToken: string; | |
| refreshToken: string; | |
| }>('/auth/refresh', { refreshToken }); | |
| localStorage.setItem('accessToken', data.accessToken); | |
| localStorage.setItem('refreshToken', data.refreshToken); | |
| if (originalReq.headers) { | |
| const headers = originalReq.headers as AxiosRequestHeaders; | |
| headers['Authorization'] = `Bearer ${data.accessToken}`; | |
| } | |
| return fetcher(originalReq); | |
| } catch (refreshError) { | |
| console.warn('토큰 갱신 실패:', refreshError); | |
| window.alert('실패했습니다.'); | |
| } | |
| try { | |
| const refreshToken = getRefreshToken(); | |
| if (!refreshToken) throw new Error('No refresh token available'); | |
| const { data } = await refreshFetcher.post<{ | |
| accessToken: string; | |
| refreshToken: string; | |
| }>('/auth/refresh', { refreshToken }); | |
| if (typeof window !== 'undefined') { | |
| localStorage.setItem('accessToken', data.accessToken); | |
| localStorage.setItem('refreshToken', data.refreshToken); | |
| } | |
| if (originalReq.headers) { | |
| const headers = originalReq.headers as AxiosRequestHeaders; | |
| headers['Authorization'] = `Bearer ${data.accessToken}`; | |
| } | |
| return fetcher(originalReq); | |
| } catch (refreshError) { | |
| console.warn('토큰 갱신 실패:', refreshError); | |
| if (typeof window !== 'undefined') { | |
| window.alert('세션이 만료되었습니다. 다시 로그인해주세요.'); | |
| } | |
| // 로그인 페이지로 리다이렉트 또는 로그아웃 처리 | |
| throw refreshError; | |
| } |
🤖 Prompt for AI Agents
In src/libs/api.ts lines 73 to 94, the token refresh logic has multiple issues:
move the session expiration alert to after a successful token refresh to avoid
confusing users; add a check to ensure localStorage is accessible (e.g., confirm
window is defined) before accessing it to prevent SSR errors; on refresh
failure, throw the error to stop retrying the original request; and replace the
vague failure alert with a clearer, more informative message. Implement these
changes to improve user experience and error handling.
| const { data } = await refreshFetcher.post<{ | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| }>('/auth/refresh', { refreshToken }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❗ 아마 이부분이 refresh 토큰을 받아오는 부분으로 예상이 가는데 경로는 /auth/tokens이고요 body를 통해 refresh token을 보내는 게 아니라 header에 넣어서 보내는 것 같더라고요 현재 요청에서 404오류가 발생하니 수정 부탁드립니다!
📌 변경 사항 개요
Access Token 만료 시 Refresh Token으로 재발급하는 코드를 추가했습니다.
📝 상세 내용
401 응답을 받으면(액세스 토큰 만료)
_retry 플래그로 무한 재시도를 방지
refreshToken이 없으면 Error를 던지고
브라우저 기본 경고창으로 세션 만료 안내
별도 Axios 인스턴스(refreshFetcher)를 사용해 /auth/refresh 호출
성공 시 새 accessToken, refreshToken을 localStorage에 저장
원래 요청의 Authorization 헤더만 교체하고 재시도
🔗 관련 이슈
🖼️ 스크린샷(선택사항)
💡 참고 사항
Summary by CodeRabbit
버그 수정
기타