[USER] RTR(Refresh Token Rotation) 인증 시스템 구현#32
Conversation
Summary by CodeRabbit
Walkthrough이번 변경에서는 리프레시 토큰 기반 인증 및 자동 토큰 갱신 기능이 추가되었습니다. 새로운 엔티티, 서비스, 리포지토리, DTO, 유틸리티가 도입되었으며, 기존 보안 설정 및 필터, 컨트롤러, OAuth2 핸들러, 프론트엔드 코드가 리프레시 토큰 처리 로직을 반영하도록 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant 브라우저
participant 서버(UsersApiController)
participant RefreshTokenService
participant CookieUtils
브라우저->>서버(UsersApiController): POST /api/auth/refresh (refresh_token 쿠키 포함)
서버(UsersApiController)->>RefreshTokenService: refreshAccessToken(refreshTokenId)
RefreshTokenService->>RefreshTokenService: 토큰 유효성/만료/사용 여부 확인
RefreshTokenService->>RefreshTokenService: 새 access/refresh 토큰 생성, 기존 토큰 사용 처리
RefreshTokenService-->>서버(UsersApiController): RefreshTokenResponseDTO 반환
서버(UsersApiController)->>CookieUtils: setTokenCookies(response, access, refresh)
CookieUtils-->>서버(UsersApiController): 쿠키 설정
서버(UsersApiController)-->>브라우저: 200 OK (쿠키 포함)
sequenceDiagram
participant 브라우저
participant 서버(LoginFilter)
participant UsersRepository
participant RefreshTokenService
participant CookieUtils
브라우저->>서버(LoginFilter): 로그인 요청
서버(LoginFilter)->>UsersRepository: 사용자 조회
서버(LoginFilter)->>RefreshTokenService: createRefreshToken(user)
RefreshTokenService-->>서버(LoginFilter): refreshTokenId 반환
서버(LoginFilter)->>CookieUtils: setTokenCookies(response, access, refresh)
CookieUtils-->>서버(LoginFilter): 쿠키 설정
서버(LoginFilter)-->>브라우저: 로그인 성공 (쿠키 포함)
Poem
Note ⚡️ AI Code Reviews for VS Code, Cursor, WindsurfCodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback. ✨ Finishing Touches
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. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (7)
src/main/resources/templates/login.html (1)
128-130: 주석 처리된 코드를 정리해주세요.로그인 성공 시 JWT 토큰을 localStorage에 저장하던 로직이 제거되어 HTTP-only 쿠키 기반 토큰 관리로 전환된 것은 보안상 훌륭한 개선입니다. 하지만 130번째 줄의 주석 처리된 중괄호는 정리가 필요합니다.
다음과 같이 정리해주세요:
if (response.ok) { window.location.href = '/'; - // } } else {src/main/java/io/github/petty/users/repository/RefreshTokenRepository.java (1)
20-25: 데이터베이스 성능 최적화를 위한 인덱스 추가 권장쿼리 메서드들이 잘 구성되어 있지만, 성능 최적화를 위해 다음 인덱스들을 고려해보세요:
findActiveTokensByProviderAndUsername- 여러 테이블 조인과 필터링이 발생findActiveTokensByUser- 시간 범위 조건과 정렬이 포함RefreshToken 엔티티에 다음 인덱스들을 추가하는 것을 권장합니다:
@Entity @Table(indexes = { @Index(name = "idx_refresh_token_user_used_expired", columnList = "user_id, used, expiredAt"), @Index(name = "idx_refresh_token_used_created", columnList = "used, createdAt") }) public class RefreshToken { // ... }또한 정기적인 만료된 토큰 정리를 위한 배치 작업도 고려해보세요.
src/main/java/io/github/petty/users/entity/RefreshToken.java (1)
45-47: 만료 검사 로직이 명확하게 구현됨현재 구현이 직관적이고 효과적입니다. 만약 테스트 용이성을 더 높이고 싶다면 서비스 레벨에서 Clock을 주입받는 방식도 고려할 수 있지만, 현재 구현도 충분히 실용적입니다.
src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java (2)
35-37: 토큰 만료 시간을 설정 파일로 관리하는 것이 좋습니다.현재 액세스 토큰 만료 시간이 하드코딩되어 있습니다. 환경별로 다른 값을 사용할 수 있도록 설정 파일로 이동하는 것을 고려해보세요.
설정 파일로 토큰 만료 시간을 이동하는 리팩토링을 도와드릴까요?
40-40: 메서드명과 파라미터가 일치하지 않습니다.
findByUsername()메서드에 이메일을 전달하고 있습니다. 메서드명이 혼란을 줄 수 있으므로findByEmail()로 변경하거나, 적절한 메서드를 사용하는 것이 좋습니다.src/main/java/io/github/petty/users/service/RefreshTokenService.java (1)
35-35: MAX_TOKENS_PER_USER를 클래스 상수로 정의하는 것이 좋습니다.메서드 내부에 정의된 상수를 클래스 레벨로 이동하면 가독성과 유지보수성이 향상됩니다.
다음과 같이 수정하세요:
@Service @RequiredArgsConstructor public class RefreshTokenService { private static final Logger logger = LoggerFactory.getLogger(RefreshTokenService.class); + private static final int MAX_TOKENS_PER_USER = 3; private final RefreshTokenRepository refreshTokenRepository; private final JWTUtil jwtUtil; // ... @Transactional public UUID createRefreshToken(Users user) { // ... - final int MAX_TOKENS_PER_USER = 3; if (activeTokens.size() >= MAX_TOKENS_PER_USER) {src/main/java/io/github/petty/users/controller/UsersApiController.java (1)
77-104: 리프레시 토큰 엔드포인트가 보안을 고려하여 잘 구현되었습니다.구현의 강점:
@CookieValue를 통한 HTTP-only 쿠키에서 토큰 추출로 XSS 방어required = false설정으로 null 체크 가능- UUID 형식 검증과 적절한 예외 처리
- 서비스 레이어 활용으로 관심사 분리
- 일관된 응답 형식과 적절한 HTTP 상태 코드
보안 고려사항도 잘 반영되어 있고, 에러 처리도 세분화되어 있어 클라이언트에서 적절히 대응할 수 있습니다.
다만 다음 개선사항을 고려해보시기 바랍니다:
@PostMapping("/auth/refresh") public ResponseEntity<?> refreshToken( @CookieValue(value = "refresh_token", required = false) String refreshToken, HttpServletResponse response) { + // 로깅 추가 고려 + log.debug("리프레시 토큰 갱신 요청 수신"); if (refreshToken == null) { + log.warn("리프레시 토큰이 제공되지 않음"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(Map.of("error", "리프레시 토큰이 없습니다.")); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/main/java/io/github/petty/config/SecurityConfig.java(4 hunks)src/main/java/io/github/petty/users/controller/UsersApiController.java(3 hunks)src/main/java/io/github/petty/users/dto/RefreshTokenResponseDTO.java(1 hunks)src/main/java/io/github/petty/users/entity/RefreshToken.java(1 hunks)src/main/java/io/github/petty/users/jwt/JWTFilter.java(1 hunks)src/main/java/io/github/petty/users/jwt/JWTUtil.java(2 hunks)src/main/java/io/github/petty/users/jwt/LoginFilter.java(3 hunks)src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java(2 hunks)src/main/java/io/github/petty/users/repository/RefreshTokenRepository.java(1 hunks)src/main/java/io/github/petty/users/service/RefreshTokenService.java(1 hunks)src/main/java/io/github/petty/users/util/CookieUtils.java(1 hunks)src/main/resources/templates/index.html(1 hunks)src/main/resources/templates/login.html(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java (2)
src/main/java/io/github/petty/users/util/CookieUtils.java (1)
CookieUtils(8-31)src/main/java/io/github/petty/users/jwt/JWTUtil.java (1)
Component(17-66)
src/main/java/io/github/petty/users/entity/RefreshToken.java (1)
src/main/java/io/github/petty/users/dto/RefreshTokenResponseDTO.java (1)
Getter(7-13)
src/main/java/io/github/petty/users/controller/UsersApiController.java (1)
src/main/java/io/github/petty/users/util/CookieUtils.java (1)
CookieUtils(8-31)
src/main/java/io/github/petty/users/jwt/LoginFilter.java (1)
src/main/java/io/github/petty/users/util/CookieUtils.java (1)
CookieUtils(8-31)
🪛 ast-grep (0.38.1)
src/main/java/io/github/petty/users/util/CookieUtils.java
[warning] 21-21: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(jwtCookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 28-28: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(refreshCookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 21-21: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(jwtCookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
[warning] 28-28: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(refreshCookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
🔇 Additional comments (13)
src/main/java/io/github/petty/users/jwt/JWTUtil.java (1)
56-65: 리프레시 토큰 추출 메서드가 잘 구현되었습니다.새로 추가된
extractRefreshToken메서드는 쿠키에서 리프레시 토큰을 안전하게 추출합니다. null 체크와 쿠키 순회 로직이 적절히 구현되어 RTR 인증 시스템을 잘 지원합니다.src/main/java/io/github/petty/users/jwt/JWTFilter.java (1)
32-36: 리프레시 엔드포인트에 대한 JWT 검증 건너뛰기 로직이 올바르게 구현되었습니다.
/api/auth/refresh엔드포인트는 액세스 토큰이 만료되었을 때 사용되는 것이므로 JWT 검증을 건너뛰는 것이 논리적으로 올바릅니다. 리프레시 토큰의 유효성은RefreshTokenService에서 별도로 검증되므로 보안상 문제가 없습니다.src/main/java/io/github/petty/users/dto/RefreshTokenResponseDTO.java (1)
1-13: 간단하고 명확한 DTO 구현입니다.리프레시 토큰 응답을 위한 DTO가 깔끔하게 구현되었습니다. Lombok 어노테이션을 적절히 사용하여 보일러플레이트 코드를 줄였고, 필요한 필드들이 잘 정의되었습니다.
src/main/resources/templates/index.html (1)
86-127: 전반적으로 잘 구현된 토큰 갱신 로직RTR 구현이 프론트엔드에서 투명하게 처리되어 사용자 경험이 향상되었습니다. async/await 패턴의 일관된 사용과 적절한 에러 처리가 돋보입니다.
src/main/java/io/github/petty/users/jwt/LoginFilter.java (1)
31-32: 의존성 주입이 적절히 구현됨RefreshTokenService와 UsersRepository 의존성이 깔끔하게 추가되었고, 생성자 매개변수 순서도 논리적입니다.
Also applies to: 38-38, 41-42
src/main/java/io/github/petty/users/repository/RefreshTokenRepository.java (1)
16-29: 잘 설계된 리포지토리 인터페이스Spring Data JPA 컨벤션을 잘 따르고 있고, 메서드 이름이 명확하여 용도를 쉽게 파악할 수 있습니다. 쿼리 구성도 논리적이고 RTR 요구사항을 잘 충족합니다.
src/main/java/io/github/petty/users/entity/RefreshToken.java (2)
13-37: 잘 설계된 엔티티 구조UUID를 기본키로 사용한 것은 보안상 좋은 선택이며, ManyToOne 관계에서 지연 로딩 설정과 적절한 제약조건들이 잘 구성되어 있습니다. Lombok 어노테이션 사용으로 보일러플레이트 코드도 효과적으로 줄였습니다.
40-42: 적절한 도메인 로직 캡슐화토큰 사용 상태를 변경하는 로직이 엔티티에 적절히 캡슐화되어 있습니다.
src/main/java/io/github/petty/config/SecurityConfig.java (2)
66-66: 리프레시 토큰 엔드포인트 접근 권한 설정이 적절합니다.
/api/auth/refresh엔드포인트를 인증 없이 접근 가능하도록 설정한 것이 적절합니다. 이를 통해 액세스 토큰이 만료되었을 때도 리프레시 토큰으로 갱신할 수 있습니다.
78-78: 로그아웃 시 리프레시 토큰 쿠키 삭제가 적절히 추가되었습니다.로그아웃 시
refresh_token쿠키를 삭제하도록 설정한 것이 보안상 적절합니다.src/main/java/io/github/petty/users/service/RefreshTokenService.java (1)
27-54: 리프레시 토큰 생성 로직이 잘 구현되었습니다.RTR(Refresh Token Rotation) 구현과 사용자당 토큰 개수 제한(3개)이 적절히 구현되었습니다. 이는 보안을 강화하고 토큰 관리를 효율적으로 만듭니다.
src/main/java/io/github/petty/users/controller/UsersApiController.java (2)
4-4: 새로운 import문들이 적절하게 추가되었습니다.리프레시 토큰 기능 구현에 필요한 모든 클래스들이 올바르게 import되었습니다:
RefreshTokenResponseDTO: 토큰 갱신 응답 DTORefreshTokenService: 리프레시 토큰 비즈니스 로직 서비스CookieUtils: 쿠키 설정 유틸리티- 서블릿 관련 클래스들과
UUID: 쿠키 처리 및 토큰 ID 처리용Also applies to: 8-12, 22-22
30-30: RefreshTokenService 의존성 주입이 올바르게 구현되었습니다.생성자 기반 의존성 주입을 통해
RefreshTokenService가 적절히 추가되었으며, 기존 의존성들과 일관된 방식으로 구현되었습니다.Also applies to: 33-33, 36-36
| } else if (response.status === 401) { | ||
| // 엑세스 토큰 만료: 자동 갱신 시도 | ||
| console.log('액세스 토큰 만료, 리프레시 토큰으로 갱신 시도'); | ||
|
|
||
| try { | ||
| const refreshResponse = await fetch('/api/auth/refresh', { | ||
| method: 'POST' | ||
| }); | ||
|
|
||
| if (refreshResponse.ok) { | ||
| console.log('토큰 갱신 성공! 원래 요청 재시도'); | ||
| // 토큰 갱신 성공 → 원래 요청 재시도 | ||
| return await checkLoginStatus(); | ||
| } else { | ||
| console.log('리프레시 토큰도 만료, 로그인 필요'); | ||
| showLoginMenu(); | ||
| } | ||
| } catch (refreshError) { | ||
| console.error('토큰 갱신 실패:', refreshError); | ||
| showLoginMenu(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
토큰 갱신 로직에 재시도 제한 및 오류 처리 개선 권장
현재 구현은 토큰 갱신 후 재귀 호출로 원래 요청을 재시도하는데, 잠재적인 문제점들이 있습니다:
- 무한 루프 위험: 갱신 엔드포인트에서 예상치 못한 401이 반복될 경우 무한 재귀가 발생할 수 있습니다
- 네트워크 오류 처리 부족: 네트워크 연결 문제 시 적절한 사용자 피드백이 없습니다
다음과 같이 개선하는 것을 권장합니다:
+let refreshAttempted = false;
async function checkLoginStatus() {
try {
const response = await fetch('/api/users/me');
if (response.ok) {
// 성공: 로그인 상태 표시
+ refreshAttempted = false; // 성공 시 플래그 리셋
const data = await response.json();
document.getElementById('userMenu').style.display = 'inline';
document.getElementById('loginMenu').style.display = 'none';
document.getElementById('userInfo').style.display = 'block';
document.getElementById('username').textContent = data.username;
document.getElementById('role').textContent = data.role;
- } else if (response.status === 401) {
+ } else if (response.status === 401 && !refreshAttempted) {
+ refreshAttempted = true;
// 액세스 토큰 만료: 자동 갱신 시도
console.log('액세스 토큰 만료, 리프레시 토큰으로 갱신 시도');
try {
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST'
});
if (refreshResponse.ok) {
console.log('토큰 갱신 성공! 원래 요청 재시도');
// 토큰 갱신 성공 → 원래 요청 재시도
return await checkLoginStatus();
} else {
console.log('리프레시 토큰도 만료, 로그인 필요');
showLoginMenu();
}
} catch (refreshError) {
console.error('토큰 갱신 실패:', refreshError);
+ alert('네트워크 연결을 확인해주세요.');
showLoginMenu();
}
} else {
// 다른 에러
showLoginMenu();
}
} catch (error) {
console.error('사용자 정보 조회 실패:', error);
+ alert('서버와의 연결에 문제가 발생했습니다.');
showLoginMenu();
}
}📝 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.
| } else if (response.status === 401) { | |
| // 엑세스 토큰 만료: 자동 갱신 시도 | |
| console.log('액세스 토큰 만료, 리프레시 토큰으로 갱신 시도'); | |
| try { | |
| const refreshResponse = await fetch('/api/auth/refresh', { | |
| method: 'POST' | |
| }); | |
| if (refreshResponse.ok) { | |
| console.log('토큰 갱신 성공! 원래 요청 재시도'); | |
| // 토큰 갱신 성공 → 원래 요청 재시도 | |
| return await checkLoginStatus(); | |
| } else { | |
| console.log('리프레시 토큰도 만료, 로그인 필요'); | |
| showLoginMenu(); | |
| } | |
| } catch (refreshError) { | |
| console.error('토큰 갱신 실패:', refreshError); | |
| showLoginMenu(); | |
| } | |
| let refreshAttempted = false; | |
| async function checkLoginStatus() { | |
| try { | |
| const response = await fetch('/api/users/me'); | |
| if (response.ok) { | |
| // 성공: 로그인 상태 표시 | |
| refreshAttempted = false; // 성공 시 플래그 리셋 | |
| const data = await response.json(); | |
| document.getElementById('userMenu').style.display = 'inline'; | |
| document.getElementById('loginMenu').style.display = 'none'; | |
| document.getElementById('userInfo').style.display = 'block'; | |
| document.getElementById('username').textContent = data.username; | |
| document.getElementById('role').textContent = data.role; | |
| } else if (response.status === 401 && !refreshAttempted) { | |
| refreshAttempted = true; | |
| // 액세스 토큰 만료: 자동 갱신 시도 | |
| console.log('액세스 토큰 만료, 리프레시 토큰으로 갱신 시도'); | |
| try { | |
| const refreshResponse = await fetch('/api/auth/refresh', { | |
| method: 'POST' | |
| }); | |
| if (refreshResponse.ok) { | |
| console.log('토큰 갱신 성공! 원래 요청 재시도'); | |
| // 토큰 갱신 성공 → 원래 요청 재시도 | |
| return await checkLoginStatus(); | |
| } else { | |
| console.log('리프레시 토큰도 만료, 로그인 필요'); | |
| showLoginMenu(); | |
| } | |
| } catch (refreshError) { | |
| console.error('토큰 갱신 실패:', refreshError); | |
| alert('네트워크 연결을 확인해주세요.'); | |
| showLoginMenu(); | |
| } | |
| } else { | |
| // 다른 에러 | |
| showLoginMenu(); | |
| } | |
| } catch (error) { | |
| console.error('사용자 정보 조회 실패:', error); | |
| alert('서버와의 연결에 문제가 발생했습니다.'); | |
| showLoginMenu(); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/resources/templates/index.html around lines 98 to 118, the token
refresh logic retries the original request recursively without limiting retries,
risking infinite loops if the refresh endpoint returns 401 repeatedly, and lacks
user feedback on network errors. To fix this, implement a retry limit counter to
prevent infinite recursion and add user-friendly error handling for network
failures during token refresh, ensuring appropriate messages or UI updates
inform the user of the issue.
| // 액세스 토큰 생성 | ||
| String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime | ||
|
|
||
| // JWT 토큰을 쿠키에 저장 | ||
| Cookie jwtCookie = new Cookie("jwt", token); | ||
| jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) | ||
| jwtCookie.setPath("/"); // 쿠키의 유효 경로 | ||
| // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) | ||
| int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환 | ||
| jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정 | ||
| response.addCookie(jwtCookie); | ||
| // 사용자 조회 | ||
| Users user = usersRepository.findByUsername(username); | ||
|
|
||
| // 리프레시 토큰 생성 | ||
| UUID refreshToken = refreshTokenService.createRefreshToken(user); | ||
|
|
||
| // response.addHeader("Authorization", "Bearer " + token); | ||
| // 쿠키 설정 코드 | ||
| CookieUtils.setTokenCookies(response, token, refreshToken); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
사용자 조회 시 null 체크 및 예외 처리 추가 필요
현재 구현에서 usersRepository.findByUsername(username)의 결과에 대한 null 체크가 없습니다. 인증이 성공했다면 사용자가 존재해야 하지만, 동시성 문제나 예상치 못한 상황에서 안전성을 위해 체크가 필요합니다.
다음과 같이 개선하는 것을 권장합니다:
// 사용자 조회
Users user = usersRepository.findByUsername(username);
+if (user == null) {
+ throw new RuntimeException("인증된 사용자를 찾을 수 없습니다: " + username);
+}
// 리프레시 토큰 생성
UUID refreshToken = refreshTokenService.createRefreshToken(user);
// 쿠키 설정 코드
CookieUtils.setTokenCookies(response, token, refreshToken);또는 Optional을 반환하는 메서드로 변경하는 것을 고려해보세요:
-Users user = usersRepository.findByUsername(username);
+Users user = usersRepository.findByUsername(username)
+ .orElseThrow(() -> new RuntimeException("인증된 사용자를 찾을 수 없습니다: " + username));📝 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.
| // 액세스 토큰 생성 | |
| String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime | |
| // JWT 토큰을 쿠키에 저장 | |
| Cookie jwtCookie = new Cookie("jwt", token); | |
| jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) | |
| jwtCookie.setPath("/"); // 쿠키의 유효 경로 | |
| // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) | |
| int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환 | |
| jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정 | |
| response.addCookie(jwtCookie); | |
| // 사용자 조회 | |
| Users user = usersRepository.findByUsername(username); | |
| // 리프레시 토큰 생성 | |
| UUID refreshToken = refreshTokenService.createRefreshToken(user); | |
| // response.addHeader("Authorization", "Bearer " + token); | |
| // 쿠키 설정 코드 | |
| CookieUtils.setTokenCookies(response, token, refreshToken); | |
| } | |
| // 액세스 토큰 생성 | |
| String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime | |
| // 사용자 조회 | |
| Users user = usersRepository.findByUsername(username); | |
| if (user == null) { | |
| throw new RuntimeException("인증된 사용자를 찾을 수 없습니다: " + username); | |
| } | |
| // 리프레시 토큰 생성 | |
| UUID refreshToken = refreshTokenService.createRefreshToken(user); | |
| // 쿠키 설정 코드 | |
| CookieUtils.setTokenCookies(response, token, refreshToken); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/jwt/LoginFilter.java around lines 78 to
89, add a null check after retrieving the user with
usersRepository.findByUsername(username). If the user is null, throw an
appropriate exception or handle the error to prevent null pointer issues.
Alternatively, modify the repository method to return an Optional and handle the
absence of the user accordingly before proceeding with refresh token creation
and cookie setting.
| // 사용자 조회 | ||
| Users user = usersRepository.findByUsername(oAuth2User.getEmail()); | ||
|
|
There was a problem hiding this comment.
사용자 조회 결과에 대한 null 체크가 필요합니다.
usersRepository.findByUsername()이 null을 반환할 수 있으므로, 리프레시 토큰 생성 전에 사용자 존재 여부를 확인해야 합니다.
다음과 같이 수정하세요:
// 사용자 조회
Users user = usersRepository.findByUsername(oAuth2User.getEmail());
+ if (user == null) {
+ throw new IllegalStateException("사용자를 찾을 수 없습니다: " + oAuth2User.getEmail());
+ }📝 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.
| // 사용자 조회 | |
| Users user = usersRepository.findByUsername(oAuth2User.getEmail()); | |
| // 사용자 조회 | |
| Users user = usersRepository.findByUsername(oAuth2User.getEmail()); | |
| if (user == null) { | |
| throw new IllegalStateException("사용자를 찾을 수 없습니다: " + oAuth2User.getEmail()); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java around
lines 39 to 41, add a null check after calling
usersRepository.findByUsername(oAuth2User.getEmail()) to verify if the user
exists before proceeding with refresh token creation. If the user is null,
handle this case appropriately, such as by throwing an exception or returning an
error response, to prevent null pointer exceptions later in the code.
| public static void setTokenCookies(HttpServletResponse response, String accessToken, UUID refreshToken) { | ||
| // 액세스 토큰을 쿠키에 저장 | ||
| Cookie jwtCookie = new Cookie("jwt", accessToken); | ||
| jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) | ||
| jwtCookie.setPath("/"); // 쿠키의 유효 경로 | ||
| // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) | ||
| jwtCookie.setMaxAge(3600);// 쿠키의 만료 시간 설정 (1시간) | ||
| response.addCookie(jwtCookie); | ||
|
|
||
| // 리프레시 토큰을 쿠키에 저장 | ||
| Cookie refreshCookie = new Cookie("refresh_token", refreshToken.toString()); | ||
| refreshCookie.setHttpOnly(true); | ||
| refreshCookie.setPath("/"); | ||
| refreshCookie.setMaxAge(7 * 24 * 60 * 60); // 7일 | ||
| response.addCookie(refreshCookie); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
환경별로 Secure 플래그를 설정할 수 있도록 개선이 필요합니다.
프로덕션 환경에서는 Secure 플래그가 필수적입니다. 환경 변수나 프로필을 통해 동적으로 설정하는 것이 좋습니다.
다음과 같이 환경별 설정을 추가할 수 있습니다:
public static void setTokenCookies(HttpServletResponse response, String accessToken, UUID refreshToken, boolean isProduction) {
// 액세스 토큰 쿠키
Cookie jwtCookie = new Cookie("jwt", accessToken);
jwtCookie.setHttpOnly(true);
jwtCookie.setPath("/");
jwtCookie.setAttribute("SameSite", "Strict");
if (isProduction) {
jwtCookie.setSecure(true);
}
jwtCookie.setMaxAge(3600);
response.addCookie(jwtCookie);
// 리프레시 토큰 쿠키
Cookie refreshCookie = new Cookie("refresh_token", refreshToken.toString());
refreshCookie.setHttpOnly(true);
refreshCookie.setPath("/");
refreshCookie.setAttribute("SameSite", "Strict");
if (isProduction) {
refreshCookie.setSecure(true);
}
refreshCookie.setMaxAge(7 * 24 * 60 * 60);
response.addCookie(refreshCookie);
}🧰 Tools
🪛 ast-grep (0.38.1)
[warning] 21-21: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(jwtCookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 28-28: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(refreshCookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 21-21: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(jwtCookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
[warning] 28-28: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(refreshCookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/util/CookieUtils.java around lines 15 to
30, improve the setTokenCookies method to accept a boolean parameter indicating
if the environment is production. Use this parameter to conditionally set the
Secure flag on both cookies. Also, add the SameSite attribute with value
"Strict" to enhance security. This allows the Secure flag to be enabled only in
production while keeping it disabled in development or localhost environments.
| // 액세스 토큰을 쿠키에 저장 | ||
| Cookie jwtCookie = new Cookie("jwt", accessToken); | ||
| jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) | ||
| jwtCookie.setPath("/"); // 쿠키의 유효 경로 | ||
| // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) | ||
| jwtCookie.setMaxAge(3600);// 쿠키의 만료 시간 설정 (1시간) | ||
| response.addCookie(jwtCookie); |
There was a problem hiding this comment.
CSRF 보호를 위해 SameSite 속성을 추가해야 합니다.
현재 쿠키에 SameSite 속성이 설정되지 않아 CSRF 공격에 취약할 수 있습니다.
다음과 같이 수정하세요:
Cookie jwtCookie = new Cookie("jwt", accessToken);
jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어)
jwtCookie.setPath("/"); // 쿠키의 유효 경로
+ jwtCookie.setAttribute("SameSite", "Strict"); // CSRF 보호
// jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략)
jwtCookie.setMaxAge(3600);// 쿠키의 만료 시간 설정 (1시간)Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 ast-grep (0.38.1)
[warning] 21-21: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(jwtCookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 21-21: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(jwtCookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/util/CookieUtils.java around lines 16 to
22, the cookie being set lacks the SameSite attribute, which is important for
CSRF protection. Modify the code to include the SameSite attribute by either
using a method to set it if available or by adding the attribute manually to the
Set-Cookie header. Ensure the SameSite attribute is set to "Strict" or "Lax"
based on your security requirements to mitigate CSRF risks.
| // 리프레시 토큰을 쿠키에 저장 | ||
| Cookie refreshCookie = new Cookie("refresh_token", refreshToken.toString()); | ||
| refreshCookie.setHttpOnly(true); | ||
| refreshCookie.setPath("/"); | ||
| refreshCookie.setMaxAge(7 * 24 * 60 * 60); // 7일 | ||
| response.addCookie(refreshCookie); |
There was a problem hiding this comment.
리프레시 토큰 쿠키에도 SameSite 속성을 추가해야 합니다.
리프레시 토큰 쿠키도 CSRF 공격으로부터 보호되어야 합니다.
다음과 같이 수정하세요:
Cookie refreshCookie = new Cookie("refresh_token", refreshToken.toString());
refreshCookie.setHttpOnly(true);
refreshCookie.setPath("/");
+ refreshCookie.setAttribute("SameSite", "Strict"); // CSRF 보호
refreshCookie.setMaxAge(7 * 24 * 60 * 60); // 7일Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 ast-grep (0.38.1)
[warning] 28-28: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(refreshCookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 28-28: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(refreshCookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/util/CookieUtils.java around lines 24 to
29, the refresh token cookie is missing the SameSite attribute, which is
important for CSRF protection. Modify the code to set the SameSite attribute on
the refreshCookie, typically by using response headers or a cookie attribute
setter if available, to enforce SameSite=Lax or SameSite=Strict as appropriate
for your security requirements.
| RefreshToken refreshToken = refreshTokenRepository.findByIdAndUsedIsFalse(refreshTokenId) | ||
| .orElseThrow(() -> new RuntimeException("유효하지 않은 리프레시 토큰입니다.")); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
RuntimeException 대신 커스텀 예외를 사용하는 것이 좋습니다.
구체적인 예외 타입을 사용하면 예외 처리가 더 명확해지고 API 응답을 적절히 제어할 수 있습니다.
다음과 같은 커스텀 예외 클래스를 생성하는 것을 제안합니다:
public class InvalidRefreshTokenException extends RuntimeException {
public InvalidRefreshTokenException(String message) {
super(message);
}
}
public class ExpiredRefreshTokenException extends RuntimeException {
public ExpiredRefreshTokenException(String message) {
super(message);
}
}커스텀 예외 클래스 생성과 적용을 도와드릴까요?
Also applies to: 69-69
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/service/RefreshTokenService.java around
lines 59-60 and line 69, replace the generic RuntimeException with a custom
exception for clearer error handling. Create two custom exception classes,
InvalidRefreshTokenException and ExpiredRefreshTokenException, each extending
RuntimeException with a constructor accepting a message. Then, update the code
to throw InvalidRefreshTokenException when the refresh token is invalid and
ExpiredRefreshTokenException when the token is expired, improving exception
specificity and API response control.
| // 액세스 토큰 및 리프레시 토큰 갱신 | ||
| @Transactional | ||
| public RefreshTokenResponseDTO refreshAccessToken(UUID refreshTokenId) { | ||
| RefreshToken refreshToken = refreshTokenRepository.findByIdAndUsedIsFalse(refreshTokenId) | ||
| .orElseThrow(() -> new RuntimeException("유효하지 않은 리프레시 토큰입니다.")); | ||
|
|
||
| // 토큰 만료 체크 | ||
| if (refreshToken.isExpired()) { | ||
| List<RefreshToken> userTokens = refreshTokenRepository.findActiveTokensByUser( | ||
| refreshToken.getUser(), LocalDateTime.now()); | ||
| refreshTokenRepository.deleteAll(userTokens); | ||
|
|
||
| logger.warn("만료된 리프레시 토큰: 사용자={}", refreshToken.getUser().getUsername()); | ||
| throw new RuntimeException("만료된 리프레시 토큰입니다."); | ||
| } | ||
|
|
||
| // 토큰 사용 처리 | ||
| refreshToken.markUsed(); | ||
| refreshTokenRepository.save(refreshToken); // 즉시 저장 | ||
|
|
||
| // 새 액세스 토큰 생성 | ||
| Users user = refreshToken.getUser(); | ||
| String newAccessToken = jwtUtil.createJwt( | ||
| user.getUsername(), | ||
| user.getRole(), | ||
| 3600000L); // 1시간 | ||
|
|
||
| // 새 리프레시 토큰 생성 | ||
| UUID newRefreshToken = createRefreshToken(user); | ||
|
|
||
| logger.debug("토큰 리프레시 성공: 사용자={}", user.getUsername()); | ||
| return new RefreshTokenResponseDTO(newAccessToken, newRefreshToken.toString()); | ||
| } |
There was a problem hiding this comment.
리프레시 토큰 갱신 시 사용자 계정 상태를 확인해야 합니다.
현재 구현에서는 사용자가 비활성화되었거나 삭제된 경우에도 토큰 갱신이 가능합니다. 보안을 강화하기 위해 사용자 상태 검증이 필요합니다.
다음과 같이 사용자 상태 확인을 추가하세요:
// 토큰 사용 처리
refreshToken.markUsed();
refreshTokenRepository.save(refreshToken); // 즉시 저장
// 새 액세스 토큰 생성
Users user = refreshToken.getUser();
+
+ // 사용자 계정 상태 확인
+ if (!user.isEnabled() || user.isDeleted()) {
+ throw new RuntimeException("비활성화되거나 삭제된 사용자입니다.");
+ }
+
String newAccessToken = jwtUtil.createJwt(참고: 위 코드는 Users 엔티티에 isEnabled(), isDeleted() 메서드가 있다고 가정합니다. 실제 구현에 맞게 조정이 필요할 수 있습니다.
📝 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.
| // 액세스 토큰 및 리프레시 토큰 갱신 | |
| @Transactional | |
| public RefreshTokenResponseDTO refreshAccessToken(UUID refreshTokenId) { | |
| RefreshToken refreshToken = refreshTokenRepository.findByIdAndUsedIsFalse(refreshTokenId) | |
| .orElseThrow(() -> new RuntimeException("유효하지 않은 리프레시 토큰입니다.")); | |
| // 토큰 만료 체크 | |
| if (refreshToken.isExpired()) { | |
| List<RefreshToken> userTokens = refreshTokenRepository.findActiveTokensByUser( | |
| refreshToken.getUser(), LocalDateTime.now()); | |
| refreshTokenRepository.deleteAll(userTokens); | |
| logger.warn("만료된 리프레시 토큰: 사용자={}", refreshToken.getUser().getUsername()); | |
| throw new RuntimeException("만료된 리프레시 토큰입니다."); | |
| } | |
| // 토큰 사용 처리 | |
| refreshToken.markUsed(); | |
| refreshTokenRepository.save(refreshToken); // 즉시 저장 | |
| // 새 액세스 토큰 생성 | |
| Users user = refreshToken.getUser(); | |
| String newAccessToken = jwtUtil.createJwt( | |
| user.getUsername(), | |
| user.getRole(), | |
| 3600000L); // 1시간 | |
| // 새 리프레시 토큰 생성 | |
| UUID newRefreshToken = createRefreshToken(user); | |
| logger.debug("토큰 리프레시 성공: 사용자={}", user.getUsername()); | |
| return new RefreshTokenResponseDTO(newAccessToken, newRefreshToken.toString()); | |
| } | |
| // 토큰 사용 처리 | |
| refreshToken.markUsed(); | |
| refreshTokenRepository.save(refreshToken); // 즉시 저장 | |
| // 새 액세스 토큰 생성 | |
| Users user = refreshToken.getUser(); | |
| // 사용자 계정 상태 확인 | |
| if (!user.isEnabled() || user.isDeleted()) { | |
| throw new RuntimeException("비활성화되거나 삭제된 사용자입니다."); | |
| } | |
| String newAccessToken = jwtUtil.createJwt( | |
| user.getUsername(), | |
| user.getRole(), | |
| 3600000L); // 1시간 | |
| // 새 리프레시 토큰 생성 | |
| UUID newRefreshToken = createRefreshToken(user); | |
| logger.debug("토큰 리프레시 성공: 사용자={}", user.getUsername()); | |
| return new RefreshTokenResponseDTO(newAccessToken, newRefreshToken.toString()); |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/service/RefreshTokenService.java between
lines 56 and 88, add a check after retrieving the user from the refresh token to
verify the user's account status. Specifically, call user.isEnabled() and
user.isDeleted() (or equivalent methods) to ensure the user is active and not
deleted. If the user is disabled or deleted, throw a RuntimeException to prevent
token refresh. This validation should be placed before marking the refresh token
as used and generating new tokens.
🎈 코드 리뷰전반적인 구현 평가 👍1. RTR 구현이 전반적으로 잘 되어있어요! 특히 다음 부분들이 인상적입니다
2. RTR 시스템의 각 시나리오에 대한 테스트 필요
결론 ✨
작업하시느라 고생하셨습니다.🚀 |
📜 PR 내용 요약
⚒️ 작업 및 변경 내용(상세하게)
1. RTR(Refresh Token Rotation) 인증 시스템 백엔드 구현
RefreshToken.java) 추가:Users엔티티와 ManyToOne 연관 관계), 인증 제공자 (provider), 생성 시간 (createdAt), 만료 시간 (expiredAt), 사용 여부 (used) 필드를 포함합니다.markUsed()메서드와 만료 여부를 확인하는isExpired()메서드를 추가했습니다.RefreshTokenRepository.java) 추가:JpaRepository를 상속받아 RefreshToken 엔티티의 데이터 접근을 담당합니다.RefreshTokenService.java) 추가:markUsed).RefreshTokenResponseDTO를 반환합니다.RefreshTokenResponseDTO추가:CookieUtils유틸리티 클래스 추가:jwt)과 Refresh Token (refresh_token) 쿠키를 설정하는 정적 메서드를 제공합니다./이며 만료 시간은 1시간(3600초)입니다./이며 만료 시간은 7일입니다.2. 인증 플로우 수정 및 RTR 연동
LoginFilter수정:CookieUtils를 통해 응답 쿠키에 설정하도록 변경했습니다.RefreshTokenService와UsersRepository빈을 추가로 주입받도록 수정했습니다.OAuth2SuccessHandler수정:LoginFilter와 마찬가지로 Access Token과 새로운 Refresh Token을 모두 생성하여CookieUtils를 통해 응답 쿠키에 설정하도록 변경했습니다.RefreshTokenService와UsersRepository빈을 추가로 주입받도록 수정했습니다.JWTFilter수정:/api/auth/refresh엔드포인트에 대한 요청은 JWT 검증 필터를 건너뛰도록 예외 처리를 추가했습니다. 이는 리프레시 토큰 갱신 시 Access Token 없이 요청하기 때문입니다.UsersApiController에/api/auth/refreshPOST 엔드포인트 추가:/api/auth/refresh경로로 POST 요청을 보낼 때 호출되는 API 엔드포인트를 추가했습니다.refresh_token값을 읽어옵니다.RefreshTokenService.refreshAccessToken메서드를 호출하여 토큰 갱신을 시도하고, 성공 시CookieUtils를 사용하여 응답 쿠키에 새로운 Access Token과 Refresh Token을 설정합니다.refresh_token이 없거나 유효하지 않은 토큰 형식인 경우, 또는 갱신 중 서비스 예외 발생 시 적절한 오류 응답을 반환합니다.SecurityConfig수정:/api/auth/refresh경로에 대한 접근을 인증 없이 허용하도록 설정했습니다./logout)에서JSESSIONID쿠키 외에jwt쿠키와refresh_token쿠키도 삭제하도록 추가했습니다.3. RTR 프론트엔드 (HTML) 구현
index.html수정:/api/users/me) 시 401 Unauthorized 응답을 받으면 (Access Token 만료 시),/api/auth/refresh엔드포인트로 자동 토큰 갱신을 시도하는 JavaScript 로직을 추가했습니다.refreshResponse.ok)checkLoginStatus함수를 재귀적으로 다시 호출하여 새로운 Access Token으로 사용자 정보를 정상적으로 조회하도록 했습니다.refreshResponse.ok가 아니거나 예외 발생 시) 로그인 메뉴를 표시하도록 했습니다.4. @Modifying 쿼리 충돌 해결
RefreshTokenService.refreshAccessToken및invalidateUserTokens메서드 수정:@Modifying쿼리를 사용하여 사용자의 모든 활성 토큰을 무효화하던 로직을 변경했습니다.@Modifying쿼리의 잠재적 충돌 문제를 피하기 위해, 먼저refreshTokenRepository.findActiveTokensByUser를 사용하여 무효화할 활성 토큰 목록을 조회한 후, 조회된 엔티티 목록을refreshTokenRepository.deleteAll메서드를 사용하여 JPQL DELETE 쿼리 대신 엔티티 단위 삭제로 변경하여 해결했습니다. 이로써 동시성 문제 발생 가능성을 줄였습니다.📚 기타 참고 사항
RTR 구현의 핵심은 사용된 리프레시 토큰을 즉시 무효화하여 토큰 하이재킹 공격 시 재사용을 방지하는 것입니다.
Refresh Token은 Access Token보다 긴 유효 기간 (7일)을 가집니다. Access Token은 1시간 유효합니다.
사용자당 최대 3개의 활성 리프레시 토큰만 유지됩니다.
로그아웃 시 발급받은
jwt및refresh_token쿠키가 모두 삭제됩니다.프론트엔드에서 Access Token 만료 시 Refresh Token으로 자동 갱신을 시도하여 사용자 경험을 개선했습니다.
@Modifying쿼리 사용 시 JPA/Hibernate의 영속성 컨텍스트 문제로 인한 잠재적 충돌 가능성이 있어, 엔티티를 먼저 조회 후 삭제하는 방식으로 우회 처리했습니다.🔧 프론트엔드 RTR 구현 방법 (다른 페이지 적용 시 참고)