Conversation
개요인증 토큰 관리 방식을 Authorization 헤더 및 localStorage 기반에서 HttpOnly 쿠키 기반으로 마이그레이션합니다. 백엔드는 액세스/리프레시 토큰을 별도의 쿠키로 발급하고, 프론트엔드는 클라이언트 측 토큰 저장소를 제거하며 쿠키 기반 요청을 사용하도록 조정됩니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
participant Client as 클라이언트
participant Backend as 백엔드 API
participant Cookie as 쿠키 저장소
rect rgba(100, 150, 200, 0.5)
Note over Client,Cookie: 신규 쿠키 기반 로그인 흐름
Client->>Backend: POST /api/auth/login
Backend->>Backend: 토큰 생성
Backend->>Cookie: Set-Cookie: access (1h), refresh (2w)
Backend-->>Client: 200 + 사용자 정보 (토큰 제외)
end
rect rgba(150, 100, 200, 0.5)
Note over Client,Cookie: 토큰 갱신
Client->>Backend: POST /api/auth/reissue (withCredentials)
Backend->>Backend: 리프레시 토큰 검증
Backend->>Cookie: Set-Cookie: access (1h 갱신)
Backend-->>Client: 200 + 메시지
end
rect rgba(200, 150, 100, 0.5)
Note over Client,Cookie: 로그아웃
Client->>Backend: POST /api/auth/logout (쿠키 포함)
Backend->>Cookie: Set-Cookie: access/refresh (maxAge=0)
Backend-->>Client: 200
Client->>Client: localStorage 정리
end
예상 코드 리뷰 소요 시간🎯 4 (복잡함) | ⏱️ ~45분 관련 가능성 있는 PR
제안 검토자
시 🐰
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 🧪 Unit Test Generation v2 is now available!We have significantly improved our unit test generation capabilities. To enable: Add this to your reviews:
finishing_touches:
unit_tests:
enabled: trueTry it out by using the Have feedback? Share your thoughts on our Discord thread! 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java (3)
105-125: API 문서의 응답 예시가 실제 응답과 불일치합니다.Swagger 문서에서 응답 예시에
accessToken과refreshToken이 포함되어 있지만, 실제 코드(lines 151-158)에서는 토큰을 제외한safeResponse를 반환합니다. 클라이언트 개발자에게 혼란을 줄 수 있습니다.📝 API 문서 수정 제안
content = `@Content`(mediaType = "application/json", examples = `@ExampleObject`(value = """ { - "accessToken": "eyJhbGciOiJIUzI1NiJ9...", - "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "userId": "1c54b9f3-8234-4e8f-b001-11cc4d9012ab", + "email": "testuser@example.com", "name": "홍길동", "role": "TEAM_MEMBER", - "phoneNumber": "01012345678" + "phoneNumber": "01012345678", + "point": 1000 } """))
347-358: 회원 탈퇴 시 Access Token 쿠키 삭제가 누락되었습니다.
logout메서드에서는access와refresh쿠키를 모두 삭제하지만,withdraw에서는refresh쿠키만 삭제합니다. 일관성을 위해 두 쿠키 모두 삭제해야 합니다.🔧 Access Token 쿠키 삭제 추가 제안
+ // Access Token 쿠키 삭제 + ResponseCookie deleteAccessCookie = ResponseCookie.from("access", "") + .httpOnly(true) + .secure(true) + .sameSite("None") + .path("/") + .maxAge(0) + .build(); + // 브라우저 쿠키 삭제 ResponseCookie deleteCookie = ResponseCookie.from("refresh", "") .httpOnly(true) .secure(true) .sameSite("None") .path("/") .maxAge(0) .build(); return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, deleteAccessCookie.toString()) .header(HttpHeaders.SET_COOKIE, deleteCookie.toString()) .body(Map.of("message", "회원 탈퇴가 완료되었습니다."));
280-296: 쿠키 삭제 시에도 환경별 설정이 필요합니다.쿠키를 삭제하려면 설정 시와 동일한 속성(
domain,path,secure,sameSite)을 사용해야 합니다. 현재secure(true)로 하드코딩되어 있어, 로컬 환경에서 쿠키가 제대로 삭제되지 않을 수 있습니다.
🤖 Fix all issues with AI agents
In
`@backend/src/main/java/org/sejongisc/backend/auth/controller/AuthController.java`:
- Around line 225-234: The cookie settings in the reissue endpoint mirror the
login endpoint and should not hardcode secure(true) and sameSite("None"); update
the ResponseCookie construction in the reissue handler (AuthController.reissue)
to read environment or configuration (e.g., a boolean like cookieSecure and a
sameSite value or active profile) and apply those values instead of constants,
falling back to secure=false and sameSite="Lax" for local/dev; ensure you still
set httpOnly(true), path("/"), and maxAge, and use the configured values when
calling ResponseCookie.from(...) and building the cookie.
- Around line 269-278: Logout currently only calls
loginService.logout(accessToken) and does not remove the stored refresh token;
add a call to refreshTokenService.deleteByUserId(...) during the logout flow to
mirror withdraw's behavior (see withdraw which calls
refreshTokenService.deleteByUserId()). Obtain the userId by extracting it from
the access token (e.g., via an existing Jwt provider or a helper like
loginService.extractUserIdFromToken(accessToken) or change loginService.logout
to return the userId) and invoke refreshTokenService.deleteByUserId(userId)
inside the same try block (keeping the existing
JwtException/IllegalArgumentException and generic exception handling).
- Around line 132-148: AuthController currently hardcodes cookie attributes when
building accessCookie and refreshCookie (ResponseCookie.from(...) with
.httpOnly(true).secure(true).sameSite("None")...), which breaks local HTTP
development; change the cookie construction in AuthController to use the same
environment-aware logic as OAuth2SuccessHandler (or centralize into a shared
utility) so that .secure and .sameSite are set conditionally based on the
runtime environment (e.g., disable secure and use a different sameSite for local
HTTP, enable secure and "None" for production/HTTPS); update the code that
builds accessCookie and refreshCookie to call that shared helper or reuse the
OAuth2SuccessHandler policy so both flows behave identically.
In `@frontend/src/components/Header.jsx`:
- Around line 30-42: The logout function currently clears local state and shows
a success toast in the finally block even when api.post('/api/auth/logout')
fails; update the logout function so that on successful
api.post('/api/auth/logout') (inside the try) you remove localStorage 'user',
call setIsLoggedIn(false), navigate (nav('/')), and show toast.success, while in
the catch you should log the error and show a toast.error (do not clear local
auth/cookies there). Keep the api.post('/api/auth/logout') call and the catch
logging but move all success-side state changes and toast.success into the try
after the awaited API call succeeds to avoid masking failures.
- Around line 10-28: The useEffect/login check can suffer from race conditions:
make checkLoginStatus respect a cleanup flag or AbortController so stale
responses don't overwrite state; inside useEffect declare a let cancelled =
false (or const controller = new AbortController()) and before calling
setIsLoggedIn or setLoading verify !cancelled (or that the fetch wasn't
aborted), and in the effect cleanup set cancelled = true (or call
controller.abort()); update the async function checkLoginStatus and its
catch/finally branches to skip state updates when cancelled to prevent outdated
responses from reverting isLoggedIn.
In `@frontend/src/components/Sidebar.jsx`:
- Around line 31-41: The login check currently sets isLoggedIn=false for all
errors; update the checkLoginStatus error handling so that after calling
api.get('/api/user/details') you only call setIsLoggedIn(false) when the caught
error indicates a 401 Unauthorized (inspect error.response?.status === 401 or
equivalent), and for other errors do not change isLoggedIn (preserve previous
state) but still run setLoading(false); modify the catch block in the
useEffect's checkLoginStatus function to perform this conditional check around
setIsLoggedIn and keep references to api.get('/api/user/details'),
checkLoginStatus, setIsLoggedIn, and setLoading to locate the code.
In `@frontend/src/pages/OAuthSuccess.jsx`:
- Around line 15-23: The code stores PII (data.email, data.phoneNumber, and
optionally name) in localStorage via localStorage.setItem('user',
JSON.stringify(...)) in OAuthSuccess.jsx; change this to persist only
non-sensitive identifiers (e.g., data.userId, data.role, data.point — omit
data.email and data.phoneNumber and consider omitting name if not needed in UI),
and update any consumers to re-fetch sensitive profile fields from the server
via an authenticated API call when required instead of reading them from
localStorage; locate the localStorage.setItem('user', ...) call and remove
email/phone (and name if appropriate), and add/ensure a profile fetch flow to
retrieve PII on-demand.
🧹 Nitpick comments (3)
backend/src/main/java/org/sejongisc/backend/common/auth/config/OAuth2SuccessHandler.java (1)
114-121:domain = "localhost"할당은 사용되지 않는 데드 코드입니다.
isProd와isDev가 모두false일 때domain = "localhost"가 할당되지만, 이후 lines 135-137, 147-149의 조건문에서 로컬 환경일 때는domain()을 설정하지 않습니다. 따라서 이 할당은 실행되어도 사용되지 않습니다.🔧 데드 코드 제거 제안
// 도메인 설정 - String domain; + String domain = null; if (isProd) { domain = "sjusisc.com"; // 운영 도메인 } else if (isDev) { domain = "sisc-web.duckdns.org"; // 개발 도메인 - } else { - domain = "localhost"; // 기본값 }frontend/src/utils/axios.js (1)
8-36: 동시 401에서 리프레시 중복 호출 방지 필요.동시 요청이 401을 반환하면
/api/auth/reissue가 병렬로 호출될 수 있어, 토큰 회전(Refresh rotation) 환경에서 실패/경합 가능성이 있습니다. 단일 refresh promise를 공유하도록 직렬화하는 편이 안전합니다.🛠️ 제안 코드
+let refreshPromise = null; + api.interceptors.response.use( (res) => res, async (err) => { const originRequest = err.config; // 액세스 토큰 만료 확인 if (err.response?.status === 401 && !originRequest._retry) { originRequest._retry = true; try { - // refreshToken은 쿠키에서 자동으로 전송됨 - await axios.post( - `${import.meta.env.VITE_API_URL}/api/auth/reissue`, - {}, // body 비움 - { withCredentials: true } - ); + if (!refreshPromise) { + // refreshToken은 쿠키에서 자동으로 전송됨 + refreshPromise = axios.post( + `${import.meta.env.VITE_API_URL}/api/auth/reissue`, + {}, // body 비움 + { withCredentials: true } + ); + } + await refreshPromise; // 새로운 accessToken은 쿠키에 자동 저장됨 // localStorage 업데이트 불필요 // Authorization 헤더 설정 불필요 // 원래 요청 재시도 return api(originRequest); } catch (refreshError) { console.error('Token refresh failed: ', refreshError); // localStorage에서 토큰 제거 불필요 (쿠키는 백엔드에서 관리) window.location.href = '/login'; return Promise.reject(refreshError); + } finally { + refreshPromise = null; } }frontend/src/hooks/useAuthGuard.js (1)
15-19: 에러 상태 판별을response.status까지 포함해 방어적으로 처리하세요.axios 에러 객체는
error.response?.status에 값이 있을 수 있습니다. 현재는error.status만 보아 누락될 수 있어 보강을 권장합니다.🛠️ 제안 코드
- if (error.status === 401) { + const status = error?.status ?? error?.response?.status; + if (status === 401) { toast.error('로그인 후 이용하실 수 있습니다.'); nav('/login'); }
🔐 쿠키 기반 JWT 인증 시스템으로 전환 및 UI 개선
📋 작업 개요
기존 localStorage 기반 JWT 토큰 관리를 HttpOnly 쿠키 기반으로 전환하여 XSS 공격에 대한 보안을 강화하고, 사이드바 및 헤더의 로그인 상태 관리를 개선했습니다.
🎯 주요 변경 사항
1. 인증 시스템 리팩토링: localStorage → HttpOnly Cookie
문제점
해결 방법
2. 백엔드 수정 내역
AuthController.java
login()메서드reissue()메서드logout()메서드OAuth2SuccessHandler.java
쿠키 도메인 설정 개선
환경별 쿠키 설정
📊 인증 플로우
일반 로그인
OAuth 로그인
API 요청
토큰 갱신
로그아웃
🧪 테스트 체크리스트
📁 수정된 파일 목록
Backend (2 files)
Frontend (6 files)
🚀 배포 시 주의사항
환경별 설정 확인
로컬 환경
secure: false(HTTP)sameSite: "Lax"domain: null(설정 안 함)개발/운영 환경
secure: true(HTTPS 필수)sameSite: "None"(Cross-Site 허용)domain: "your-domain.com"(서브도메인 공유)CORS 설정
💡 추가 개선 사항
🔗 참고 문서
- OWASP - HttpOnly Cookie
- MDN - SameSite cookies
- RFC 6749 - OAuth 2.0
# 🔐 쿠키 기반 JWT 인증 시스템으로 전환 및 UI 개선📋 작업 개요
기존 localStorage 기반 JWT 토큰 관리를 HttpOnly 쿠키 기반으로 전환하여 XSS 공격에 대한 보안을 강화하고, 사이드바 및 헤더의 로그인 상태 관리를 개선했습니다.
🎯 주요 변경 사항
1. 인증 시스템 리팩토링: localStorage → HttpOnly Cookie
문제점
해결 방법
2. 백엔드 수정 내역
AuthController.java
login()메서드reissue()메서드logout()메서드OAuth2SuccessHandler.java
쿠키 도메인 설정 개선
환경별 쿠키 설정
3. 프론트엔드 수정 내역
utils/auth.js
utils/axios.js
Request Interceptor
Response Interceptor
pages/OAuthSuccess.jsx
4. 로그인 상태 관리 개선
components/Sidebar.jsx
로그인 상태 확인
로그아웃 처리
UI 렌더링
components/Header.jsx
Sidebar와 동일한 로직으로 로그인 상태 관리 및 로그아웃 처리 구현
모바일 화면 UI
5. useAuthGuard 훅 개선
hooks/useAuthGuard.js
🔒 보안 개선 사항
📊 인증 플로우
일반 로그인
OAuth 로그인
API 요청
토큰 갱신
로그아웃
🧪 테스트 체크리스트
📁 수정된 파일 목록
Backend (2 files)
Frontend (6 files)
🚀 배포 시 주의사항
환경별 설정 확인
로컬 환경
secure: false(HTTP)sameSite: "Lax"domain: null(설정 안 함)개발/운영 환경
secure: true(HTTPS 필수)sameSite: "None"(Cross-Site 허용)domain: "your-domain.com"(서브도메인 공유)CORS 설정
💡 추가 개선 사항
🔗 참고 문서
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.