-
Notifications
You must be signed in to change notification settings - Fork 6
[USER] RTR(Refresh Token Rotation) 인증 시스템 구현 #32
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package io.github.petty.users.dto; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class RefreshTokenResponseDTO { | ||
| private String accessToken; | ||
| private String refreshToken; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package io.github.petty.users.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import org.hibernate.annotations.CreationTimestamp; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.UUID; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class RefreshToken { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.UUID) | ||
| private UUID id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id") | ||
| private Users user; | ||
|
|
||
| @Column(nullable = false) | ||
| private String provider; // 인증 제공자 (local, github 등) | ||
|
|
||
| @CreationTimestamp | ||
| private LocalDateTime createdAt; | ||
|
|
||
| @Column(nullable = false) | ||
| private LocalDateTime expiredAt; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean used; | ||
|
|
||
| // 토큰을 사용됨으로 표시 | ||
| public void markUsed() { | ||
| this.used = true; | ||
| } | ||
|
|
||
| // 토큰이 만료되었는지 확인 | ||
| public boolean isExpired() { | ||
| return LocalDateTime.now().isAfter(this.expiredAt); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,10 @@ | ||||||||||||||||
| package io.github.petty.users.oauth2; | ||||||||||||||||
|
|
||||||||||||||||
| import io.github.petty.users.entity.Users; | ||||||||||||||||
| import io.github.petty.users.jwt.JWTUtil; | ||||||||||||||||
| import io.github.petty.users.repository.UsersRepository; | ||||||||||||||||
| import io.github.petty.users.service.RefreshTokenService; | ||||||||||||||||
| import io.github.petty.users.util.CookieUtils; | ||||||||||||||||
| import jakarta.servlet.ServletException; | ||||||||||||||||
| import jakarta.servlet.http.Cookie; | ||||||||||||||||
| import jakarta.servlet.http.HttpServletRequest; | ||||||||||||||||
|
|
@@ -11,33 +15,37 @@ | |||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||||
|
|
||||||||||||||||
| import java.io.IOException; | ||||||||||||||||
| import java.util.UUID; | ||||||||||||||||
|
|
||||||||||||||||
| @Component | ||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||
| public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { | ||||||||||||||||
|
|
||||||||||||||||
| private final JWTUtil jwtUtil; | ||||||||||||||||
| private final RefreshTokenService refreshTokenService; | ||||||||||||||||
| private final UsersRepository usersRepository; | ||||||||||||||||
|
|
||||||||||||||||
| @Override | ||||||||||||||||
| public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, | ||||||||||||||||
| Authentication authentication) throws IOException, ServletException { | ||||||||||||||||
|
|
||||||||||||||||
| CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); | ||||||||||||||||
|
|
||||||||||||||||
| // JWT 발급 | ||||||||||||||||
| String token = jwtUtil.createJwt(oAuth2User.getEmail(), oAuth2User.getAuthorities().iterator().next().getAuthority(), 3600000L); | ||||||||||||||||
| // 액세스 토큰 생성 | ||||||||||||||||
| String token = jwtUtil.createJwt(oAuth2User.getEmail(), | ||||||||||||||||
| oAuth2User.getAuthorities().iterator().next().getAuthority(), | ||||||||||||||||
| 3600000L); // 1시간 | ||||||||||||||||
|
|
||||||||||||||||
| // 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(oAuth2User.getEmail()); | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+39
to
41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자 조회 결과에 대한 null 체크가 필요합니다.
다음과 같이 수정하세요: // 사용자 조회
Users user = usersRepository.findByUsername(oAuth2User.getEmail());
+ if (user == null) {
+ throw new IllegalStateException("사용자를 찾을 수 없습니다: " + oAuth2User.getEmail());
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| String targetUrl = "/"; | ||||||||||||||||
| // 리프레시 토큰 생성 | ||||||||||||||||
| UUID refreshToken = refreshTokenService.createRefreshToken(user); | ||||||||||||||||
|
|
||||||||||||||||
| // 쿠키 설정 코드 | ||||||||||||||||
| CookieUtils.setTokenCookies(response, token, refreshToken); | ||||||||||||||||
|
|
||||||||||||||||
| String targetUrl = "/"; | ||||||||||||||||
| getRedirectStrategy().sendRedirect(request, response, targetUrl); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
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.
🛠️ Refactor suggestion
사용자 조회 시 null 체크 및 예외 처리 추가 필요
현재 구현에서
usersRepository.findByUsername(username)의 결과에 대한 null 체크가 없습니다. 인증이 성공했다면 사용자가 존재해야 하지만, 동시성 문제나 예상치 못한 상황에서 안전성을 위해 체크가 필요합니다.다음과 같이 개선하는 것을 권장합니다:
또는 Optional을 반환하는 메서드로 변경하는 것을 고려해보세요:
📝 Committable suggestion
🤖 Prompt for AI Agents