-
Notifications
You must be signed in to change notification settings - Fork 1
[#15] Feat: 로그인/회원가입 API 구현 #16
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
Conversation
CustomUserDetails 구현
WalkthroughSpring Security 및 JWT 기반 인증 시스템을 구현하기 위해 의존성을 추가하고, 회원가입, 로그인, 토큰 갱신, 로그아웃 기능을 처리하는 컨트롤러, 서비스, DTO, 엔티티를 도입합니다. 보안 필터 체인과 토큰 유효성 검사 로직을 설정합니다. Changes
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
🧹 Nitpick comments (9)
src/main/java/com/example/triptalk/TriptalkApplication.java (1)
7-7:@EnableJpaAuditing을 별도의 설정 클래스로 분리하는 것을 권장합니다.메인 애플리케이션 클래스에
@EnableJpaAuditing을 선언하면@WebMvcTest,@DataJpaTest등 슬라이스 테스트에서 문제가 발생할 수 있습니다. JPA Auditing이 활성화되지만AuditingHandler빈이 로드되지 않아 테스트가 실패할 수 있습니다.별도의 설정 클래스로 분리하는 것을 권장합니다:
// src/main/java/com/example/triptalk/global/config/JpaAuditingConfig.java package com.example.triptalk.global.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration @EnableJpaAuditing public class JpaAuditingConfig { }그 후 메인 클래스에서
@EnableJpaAuditing을 제거하세요.src/main/java/com/example/triptalk/global/security/jwt/JwtProperties.java (1)
12-16: 설정 속성에 유효성 검증 추가를 권장합니다.JWT 설정은 보안에 중요한 요소이므로, 애플리케이션 시작 시 잘못된 설정을 조기에 감지할 수 있도록 유효성 검증을 추가하는 것이 좋습니다. 설정이 누락되면 런타임에 예상치 못한 오류가 발생할 수 있습니다.
+import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import org.springframework.validation.annotation.Validated; + @Getter @Setter @Component +@Validated @ConfigurationProperties(prefix = "jwt") public class JwtProperties { + @NotBlank(message = "JWT secret은 필수입니다") private String secret; + + @Positive(message = "Access token 만료 시간은 양수여야 합니다") private long accessTokenExpiration; + + @Positive(message = "Refresh token 만료 시간은 양수여야 합니다") private long refreshTokenExpiration; }src/main/java/com/example/triptalk/domain/user/entity/RefreshToken.java (2)
22-23:userId컬럼에 인덱스 추가를 권장합니다.
RefreshTokenRepository에서deleteByUserId(Long userId)메서드가 사용될 것으로 예상됩니다. 사용자별 토큰 조회/삭제 성능을 위해userId컬럼에 인덱스를 추가하는 것이 좋습니다.@Entity @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@Table(indexes = @Index(name = "idx_refresh_token_user_id", columnList = "userId")) public class RefreshToken {
28-30:isExpired()메서드의 시간대(timezone) 처리를 확인하세요.
LocalDateTime.now()는 시스템 기본 시간대를 사용합니다. 서버 배포 환경의 시간대가 다를 경우 예상치 못한 동작이 발생할 수 있습니다. 일관된 시간 처리를 위해Instant또는 명시적 시간대 사용을 고려해보세요.-import java.time.LocalDateTime; +import java.time.Instant; // ... @Column(nullable = false) - private LocalDateTime expiryDate; + private Instant expiryDate; public boolean isExpired() { - return LocalDateTime.now().isAfter(expiryDate); + return Instant.now().isAfter(expiryDate); }또는 현재 구조를 유지하면서 명시적 시간대를 사용:
public boolean isExpired() { return LocalDateTime.now(ZoneOffset.UTC).isAfter(expiryDate); }src/main/java/com/example/triptalk/global/security/CustomUserDetails.java (1)
34-52: 계정 상태 체크 메서드들이 하드코딩되어 있습니다.현재 모든 계정 상태 확인 메서드(
isAccountNonExpired,isAccountNonLocked,isCredentialsNonExpired,isEnabled)가true를 반환하도록 하드코딩되어 있습니다.초기 구현으로는 적절하지만, 향후 계정 잠금, 만료, 비활성화 등의 기능이 필요할 경우 User 엔티티에 해당 필드를 추가하고 실제 상태를 반환하도록 개선하는 것을 고려해보세요.
src/main/java/com/example/triptalk/global/security/jwt/JwtAuthenticationFilter.java (1)
50-52: 예외 처리 개선을 고려해보세요.현재는 예외 발생 시 로그만 남기고 필터 체인을 계속 진행합니다. 선택적으로, 클라이언트에게 명확한 에러 응답(예: 401 Unauthorized)을 반환하는 것을 고려해볼 수 있습니다.
다만 현재 구현도 인증 실패 시 SecurityContext에 인증 정보가 설정되지 않아 이후 필터/컨트롤러에서 자동으로 401 처리되므로, 현재 구현도 충분히 동작합니다.
src/main/java/com/example/triptalk/global/security/jwt/JwtTokenProvider.java (2)
21-23: 서명 키 생성을 최적화하세요.
getSigningKey()가 호출될 때마다 새로운SecretKey객체를 생성합니다. 이 키는 변경되지 않으므로 필드로 캐싱하여 성능을 개선할 수 있습니다.다음 diff를 적용하세요:
@Component @RequiredArgsConstructor @Getter public class JwtTokenProvider { private final JwtProperties jwtProperties; + private SecretKey signingKey; - // SecretKey 생성 - private SecretKey getSigningKey() { - return Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + private SecretKey getSigningKey() { + if (signingKey == null) { + signingKey = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + } + return signingKey; }
63-73: 토큰 검증 실패 시 원인을 구분하세요.현재
validateToken()메서드는 모든 예외를 동일하게 처리하여false를 반환합니다. 하지만AuthServiceImpl의reissue()메서드(Line 70-72, 79-82)에서는 만료된 토큰과 유효하지 않은 토큰을 구분하여 처리해야 합니다.현재 구현에서는
validateToken()이false를 반환하면AUTH_INVALID_TOKEN에러를 발생시키고, 이후 DB 조회에서 토큰 만료 여부를 확인합니다. 이는 논리적으로 작동하지만, 만료된 토큰도 서명이 유효하다면validateToken()을 통과해야 더 명확한 에러 메시지를 제공할 수 있습니다.더 명확한 에러 처리를 위해 다음과 같이 개선할 수 있습니다:
- // 토큰 유효성 검증(서명, 만료시간) - public boolean validateToken(String token) { - try { - Jwts.parser() - .verifyWith(getSigningKey()) - .build() - .parseSignedClaims(token); - return true; - } catch (JwtException | IllegalArgumentException e) { - return false; - } - } + // 토큰 서명 유효성 검증 (만료 여부는 별도 확인) + public boolean validateTokenSignature(String token) { + try { + Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token); + return true; + } catch (ExpiredJwtException e) { + // 만료된 토큰이지만 서명은 유효함 + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public boolean isTokenExpired(String token) { + try { + Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token); + return false; + } catch (ExpiredJwtException e) { + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid token"); + } + }그리고
AuthServiceImpl.reissue()에서 이를 활용하도록 수정해야 합니다.src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java (1)
47-65: 로그인 로직이 올바르게 구현되었습니다.사용자 인증, 토큰 생성, 리프레시 토큰 저장이 적절하게 처리되었습니다. 다만, 프로덕션 환경에서는 무차별 대입 공격을 방지하기 위해 로그인 시도 횟수 제한(rate limiting)을 고려하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
build.gradle(1 hunks)src/main/java/com/example/triptalk/TriptalkApplication.java(1 hunks)src/main/java/com/example/triptalk/domain/user/controller/AuthController.java(1 hunks)src/main/java/com/example/triptalk/domain/user/converter/AuthConverter.java(1 hunks)src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java(1 hunks)src/main/java/com/example/triptalk/domain/user/dto/AuthResponse.java(1 hunks)src/main/java/com/example/triptalk/domain/user/entity/RefreshToken.java(1 hunks)src/main/java/com/example/triptalk/domain/user/repository/RefreshTokenRepository.java(1 hunks)src/main/java/com/example/triptalk/domain/user/repository/UserRepository.java(1 hunks)src/main/java/com/example/triptalk/domain/user/service/AuthService.java(1 hunks)src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java(1 hunks)src/main/java/com/example/triptalk/global/apiPayload/code/status/ErrorStatus.java(1 hunks)src/main/java/com/example/triptalk/global/config/SecurityConfig.java(1 hunks)src/main/java/com/example/triptalk/global/security/CustomUserDetails.java(1 hunks)src/main/java/com/example/triptalk/global/security/CustomUserDetailsService.java(1 hunks)src/main/java/com/example/triptalk/global/security/jwt/JwtAuthenticationFilter.java(1 hunks)src/main/java/com/example/triptalk/global/security/jwt/JwtProperties.java(1 hunks)src/main/java/com/example/triptalk/global/security/jwt/JwtTokenProvider.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/example/triptalk/domain/user/repository/UserRepository.java (1)
src/main/java/com/example/triptalk/domain/user/service/UserServiceImpl.java (2)
Service(13-30)Override(20-29)
src/main/java/com/example/triptalk/domain/user/repository/RefreshTokenRepository.java (3)
src/main/java/com/example/triptalk/domain/tripPlan/repository/TripHighlightRepository.java (2)
TripHighlightRepository(8-10)findByTripPlanId(9-9)src/main/java/com/example/triptalk/domain/tripPlan/repository/TripTransportationRepository.java (1)
TripTransportationRepository(8-10)src/main/java/com/example/triptalk/domain/tripPlan/repository/TripAccommodationRepository.java (1)
TripAccommodationRepository(8-10)
src/main/java/com/example/triptalk/domain/user/converter/AuthConverter.java (3)
src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java (1)
AuthRequest(8-45)src/main/java/com/example/triptalk/domain/user/dto/AuthResponse.java (1)
AuthResponse(8-36)src/main/java/com/example/triptalk/domain/user/dto/UserResponse.java (2)
Getter(15-29)Getter(10-76)
src/main/java/com/example/triptalk/domain/user/controller/AuthController.java (2)
src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java (1)
AuthRequest(8-45)src/main/java/com/example/triptalk/domain/user/dto/AuthResponse.java (1)
AuthResponse(8-36)
src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java (3)
src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java (1)
AuthRequest(8-45)src/main/java/com/example/triptalk/domain/user/dto/AuthResponse.java (1)
AuthResponse(8-36)src/main/java/com/example/triptalk/global/apiPayload/exception/handler/ErrorHandler.java (1)
ErrorHandler(6-10)
src/main/java/com/example/triptalk/global/security/jwt/JwtTokenProvider.java (1)
src/main/java/com/example/triptalk/global/apiPayload/exception/ExceptionAdvice.java (1)
RequiredArgsConstructor(26-120)
🔇 Additional comments (12)
src/main/java/com/example/triptalk/domain/user/repository/UserRepository.java (1)
13-14: LGTM!
existsByEmail메서드는 Spring Data JPA의 파생 쿼리 네이밍 컨벤션을 올바르게 따르고 있으며, 이메일 중복 체크에 효율적인 방식입니다.findByEmail().isPresent()대신existsBy를 사용하면 불필요한 엔티티 로딩 없이SELECT COUNT또는EXISTS쿼리로 최적화됩니다.src/main/java/com/example/triptalk/domain/user/dto/AuthResponse.java (1)
8-36: LGTM!DTO 구조가 깔끔하고 Lombok 어노테이션과 Swagger 문서화가 잘 적용되어 있습니다. 정적 내부 클래스로 관련 DTO들을 그룹화한 패턴은 유지보수성을 높여줍니다.
src/main/java/com/example/triptalk/global/config/SecurityConfig.java (1)
19-44: 보안 설정이 적절하게 구성되었습니다.JWT 기반 stateless 인증을 위한 Spring Security 설정이 올바르게 구현되었습니다. CSRF 비활성화, 세션 관리 정책, 엔드포인트 권한 설정, 필터 체인 순서 모두 적절합니다.
src/main/java/com/example/triptalk/domain/user/service/AuthService.java (1)
6-15: 인증 서비스 인터페이스가 명확하게 정의되었습니다.회원가입, 로그인, 토큰 재발급, 로그아웃의 핵심 인증 기능들이 간결하게 정의되어 있습니다.
src/main/java/com/example/triptalk/global/security/CustomUserDetailsService.java (1)
11-23: UserDetailsService 구현이 적절합니다.Spring Security와의 통합을 위한 사용자 조회 로직이 올바르게 구현되었습니다.
src/main/java/com/example/triptalk/domain/user/converter/AuthConverter.java (1)
8-35: 컨버터 구현이 깔끔하게 작성되었습니다.DTO와 엔티티 간 변환 로직이 명확하고 올바르게 구현되었습니다. 빌더 패턴을 활용하여 가독성이 좋습니다.
src/main/java/com/example/triptalk/global/security/CustomUserDetails.java (1)
15-57: UserDetails 구현이 올바르게 작성되었습니다.Spring Security의 UserDetails 인터페이스를 적절하게 구현하여 도메인 User 엔티티를 감싸고 있습니다.
src/main/java/com/example/triptalk/global/security/jwt/JwtAuthenticationFilter.java (2)
26-55: JWT 인증 필터가 올바르게 구현되었습니다.토큰 추출, 검증, SecurityContext 설정 로직이 적절하게 구현되어 있습니다. 예외 처리도 포함되어 있어 안정적입니다.
58-64: 토큰 추출 로직이 명확합니다.Authorization 헤더에서 Bearer 토큰을 추출하는 로직이 올바르게 구현되었습니다.
src/main/java/com/example/triptalk/global/apiPayload/code/status/ErrorStatus.java (1)
23-28: 인증 관련 에러 코드가 적절하게 추가되었습니다.HTTP 상태 코드와 에러 메시지가 명확하고 일관성 있게 정의되었습니다.
src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java (2)
32-44: 회원가입 로직이 올바르게 구현되었습니다.이메일 중복 체크, 비밀번호 인코딩, 사용자 저장이 적절하게 처리되었습니다.
100-104: 로그아웃 로직이 올바르게 구현되었습니다.사용자의 모든 리프레시 토큰을 삭제하여 로그아웃을 처리합니다.
| implementation 'io.jsonwebtoken:jjwt-api:0.12.3' | ||
| implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' | ||
| implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check latest JJWT version on Maven Central
curl -s "https://search.maven.org/solrsearch/select?q=g:io.jsonwebtoken+AND+a:jjwt-api&rows=1&wt=json" | jq '.response.docs[0].latestVersion'Repository: TripTalk/BE_SpringBoot
Length of output: 74
JJWT 의존성을 최신 버전으로 업데이트하세요.
현재 0.12.3 버전이 사용 중이나, Maven Central의 최신 안정 버전은 0.12.6입니다. 보안 패치 및 버그 수정을 포함한 최신 버전으로 업데이트하는 것을 권장합니다.
🤖 Prompt for AI Agents
In build.gradle around lines 36 to 38, the JJWT dependencies are pinned to
0.12.3; update all three coordinates (io.jsonwebtoken:jjwt-api, jjwt-impl,
jjwt-jackson) to the latest stable 0.12.6 to pick up security and bug fixes,
then refresh/resolve Gradle dependencies (e.g., gradle --refresh-dependencies or
IDE sync) and run tests to verify no regressions.
| implementation 'io.jsonwebtoken:jjwt-api:0.12.3' | ||
| implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' | ||
| implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' | ||
| implementation 'org.springframework.boot:spring-boot-configuration-processor' |
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.
spring-boot-configuration-processor는 annotationProcessor로 선언해야 합니다.
spring-boot-configuration-processor는 컴파일 시점에 @ConfigurationProperties 클래스에 대한 메타데이터를 생성하는 어노테이션 프로세서입니다. implementation 대신 annotationProcessor로 선언해야 올바르게 동작합니다.
- implementation 'org.springframework.boot:spring-boot-configuration-processor'
+ annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'📝 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.
| implementation 'org.springframework.boot:spring-boot-configuration-processor' | |
| annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' |
🤖 Prompt for AI Agents
In build.gradle around line 39, the dependency declaration uses implementation
'org.springframework.boot:spring-boot-configuration-processor' but this library
is an annotation processor and must be declared as annotationProcessor; replace
the implementation configuration with annotationProcessor
'org.springframework.boot:spring-boot-configuration-processor' (optionally also
add a compileOnly entry if IDEs need the classes at compile time).
|
|
||
| @Operation(summary = "회원가입", description = "이메일, 비밀번호, 닉네임으로 회원가입합니다.") | ||
| @PostMapping("/signup") | ||
| public ApiResponse<AuthResponse.SignUpDTO> signUp(@RequestBody AuthRequest.SignUpDTO request) { |
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 | 🟠 Major
요청 DTO 유효성 검증을 추가하세요.
요청 본문에 대한 입력 검증이 없습니다. @Valid 어노테이션을 추가하여 DTO 필드의 유효성을 검증해야 합니다.
다음 diff를 적용하세요:
- public ApiResponse<AuthResponse.SignUpDTO> signUp(@RequestBody AuthRequest.SignUpDTO request) {
+ public ApiResponse<AuthResponse.SignUpDTO> signUp(@Valid @RequestBody AuthRequest.SignUpDTO request) {또한 AuthRequest.SignUpDTO 클래스에 적절한 검증 어노테이션(예: @NotBlank, @Email, @Size 등)을 추가해야 합니다.
📝 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.
| public ApiResponse<AuthResponse.SignUpDTO> signUp(@RequestBody AuthRequest.SignUpDTO request) { | |
| public ApiResponse<AuthResponse.SignUpDTO> signUp(@Valid @RequestBody AuthRequest.SignUpDTO request) { |
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/controller/AuthController.java
around line 24, the signUp endpoint is missing request body validation; add the
@Valid annotation to the method parameter (e.g., public
ApiResponse<AuthResponse.SignUpDTO> signUp(@Valid @RequestBody
AuthRequest.SignUpDTO request)) so Spring triggers validation, and then update
AuthRequest.SignUpDTO to include appropriate Jakarta/javax validation
annotations on its fields (e.g., @NotBlank, @Email, @Size, @NotNull as
applicable) to enforce input constraints and enable automatic 400 responses on
invalid input.
|
|
||
| @Operation(summary = "로그인", description = "이메일과 비밀번호로 로그인하여 토큰을 발급받습니다.") | ||
| @PostMapping("/login") | ||
| public ApiResponse<AuthResponse.TokenDTO> login(@RequestBody AuthRequest.LoginDTO request) { |
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 | 🟠 Major
요청 DTO 유효성 검증을 추가하세요.
로그인 요청에 대한 입력 검증이 없습니다.
다음 diff를 적용하세요:
- public ApiResponse<AuthResponse.TokenDTO> login(@RequestBody AuthRequest.LoginDTO request) {
+ public ApiResponse<AuthResponse.TokenDTO> login(@Valid @RequestBody AuthRequest.LoginDTO request) {🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/controller/AuthController.java
around line 30, the login endpoint accepts the DTO without any validation; add
request validation by annotating the controller parameter with @Valid (and
import javax/jakarta.validation.Valid) and ensure the AuthRequest.LoginDTO
fields have proper javax/jakarta.validation annotations (e.g., @NotBlank,
@Email, @Size) in its class; either add a BindingResult parameter to return a
formatted ApiResponse on validation failure or rely on an existing global
MethodArgumentNotValidException handler to convert validation errors into
ApiResponse; also add @Validated at the controller class level if needed.
|
|
||
| @Operation(summary = "토큰 재발급", description = "Refresh Token으로 Access Token을 재발급받습니다.") | ||
| @PostMapping("/refresh") | ||
| public ApiResponse<AuthResponse.TokenDTO> reissue(@RequestBody AuthRequest.ReissueDTO request) { |
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 | 🟠 Major
요청 DTO 유효성 검증을 추가하세요.
토큰 재발급 요청에 대한 입력 검증이 없습니다.
다음 diff를 적용하세요:
- public ApiResponse<AuthResponse.TokenDTO> reissue(@RequestBody AuthRequest.ReissueDTO request) {
+ public ApiResponse<AuthResponse.TokenDTO> reissue(@Valid @RequestBody AuthRequest.ReissueDTO request) {🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/controller/AuthController.java
around line 36, the reissue endpoint accepts the ReissueDTO without validation;
add request validation by annotating the parameter with @Valid (i.e., public
ApiResponse<AuthResponse.TokenDTO> reissue(@Valid @RequestBody
AuthRequest.ReissueDTO request)), import javax.validation.Valid, and annotate
the controller class with @Validated if not present. Update
AuthRequest.ReissueDTO to declare explicit validation constraints on its fields
(e.g., @NotBlank for token strings, @NotNull where appropriate) and import the
corresponding javax.validation annotations. Rely on Spring’s
MethodArgumentNotValidException to return 400 or add an exception handler if you
want custom error responses.
| public static class ReissueDTO { | ||
| @Schema(description = "리프레시 토큰") | ||
| private String 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.
토큰 재발급 DTO에도 검증이 필요합니다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "토큰 재발급 요청")
public static class ReissueDTO {
+ @NotBlank(message = "리프레시 토큰은 필수입니다")
@Schema(description = "리프레시 토큰")
private String refreshToken;
}🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/dto/AuthRequest.java around
lines 41-44, the ReissueDTO lacks validation on its refreshToken field; annotate
the field with a javax.validation constraint such as @NotBlank (with an
appropriate message) to enforce non-empty tokens, import the constraint, and
ensure any controller consuming this DTO uses @Valid on the request body so
validation is triggered.
| public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> { | ||
| Optional<RefreshToken> findByToken(String token); | ||
| Optional<RefreshToken> findByUserId(Long userId); | ||
| void deleteByUserId(Long userId); | ||
| } |
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.
삭제 메서드에 필수 어노테이션이 누락되었습니다.
deleteByUserId 메서드는 커스텀 삭제 쿼리이므로 @Modifying과 @Transactional 어노테이션이 필요합니다. 이 어노테이션들이 없으면 삭제 작업이 제대로 실행되지 않을 수 있습니다.
다음 diff를 적용하세요:
package com.example.triptalk.domain.user.repository;
import com.example.triptalk.domain.user.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByUserId(Long userId);
+
+ @Modifying
+ @Transactional
void deleteByUserId(Long userId);
}🤖 Prompt for AI Agents
In
src/main/java/com/example/triptalk/domain/user/repository/RefreshTokenRepository.java
around lines 8 to 12, the deleteByUserId method is a modifying query but is
missing the required @Modifying and @Transactional annotations; update the
repository by annotating the deleteByUserId method with @Modifying and
@Transactional (or mark the repository/class transactional) and add the
corresponding imports (org.springframework.data.jpa.repository.Modifying and
org.springframework.transaction.annotation.Transactional) so the delete
operation executes correctly.
| // 기존 RefreshToken 삭제 후 새로운 토큰 저장 | ||
| refreshTokenRepository.delete(refreshToken); | ||
| saveRefreshToken(user.getId(), newRefreshToken); |
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 | 🟠 Major
중복된 삭제 로직을 제거하세요.
Line 93에서 기존 RefreshToken을 삭제한 후, Line 94의 saveRefreshToken() 메서드 내부(Line 108)에서 다시 동일한 삭제 작업을 수행합니다. 이는 불필요한 중복입니다.
다음 중 하나를 선택하여 수정하세요:
옵션 1: reissue()에서 삭제 제거 (권장)
// 새로운 토큰 생성
String newAccessToken = jwtTokenProvider.createAccessToken(user.getEmail());
String newRefreshToken = jwtTokenProvider.createRefreshToken(user.getEmail());
- // 기존 RefreshToken 삭제 후 새로운 토큰 저장
- refreshTokenRepository.delete(refreshToken);
+ // 새로운 토큰 저장 (saveRefreshToken이 기존 토큰 삭제 처리)
saveRefreshToken(user.getId(), newRefreshToken);옵션 2: saveRefreshToken()을 upsert 방식으로 변경
현재 deleteByUserId 후 save하는 패턴보다는, 기존 토큰이 있으면 업데이트하고 없으면 생성하는 방식이 더 효율적일 수 있습니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java
around lines 92-94 (and note saveRefreshToken's internal deletion at line ~108),
the refresh token is being deleted twice: once in reissue() (line 93) and again
inside saveRefreshToken(). Remove the redundant delete from reissue() so
saveRefreshToken() remains the single place handling replacement, or
alternatively implement an upsert in saveRefreshToken() (replace
deleteByUserId+save with findByUserId and update if present, otherwise create) —
preferred: delete the explicit refreshTokenRepository.delete(refreshToken) call
in reissue(), leaving saveRefreshToken() to handle persistence consistently.
| .expiryDate(LocalDateTime.now().plusSeconds( | ||
| jwtTokenProvider.getRefreshTokenExpiration() / 1000)) |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for getRefreshTokenExpiration method definition and refreshTokenExpiration property
rg -n -A5 -B5 "getRefreshTokenExpiration|refreshTokenExpiration" --type javaRepository: TripTalk/BE_SpringBoot
Length of output: 4415
🏁 Script executed:
#!/bin/bash
# Read JwtProperties to see if there are any comments
cat -n src/main/java/com/example/triptalk/global/security/jwt/JwtProperties.java
echo "---"
# Search for JWT configuration in properties/yaml files
find . -type f \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \) -exec cat {} \; | grep -A2 -B2 "jwt:" || echo "No JWT config found in standard locations"Repository: TripTalk/BE_SpringBoot
Length of output: 667
확인된 문제: 만료 시간 계산 시 단위 변환이 명확하지 않음
getRefreshTokenExpiration()은 밀리초를 반환합니다 (JwtTokenProvider의 Date 생성자 사용법으로 확인). AuthServiceImpl에서 /1000으로 나누어 초 단위로 변환하는 것은 LocalDateTime.plusSeconds()의 요구사항에 맞게 올바르게 동작합니다.
다만 다음과 같이 개선하면 코드 의도가 더욱 명확해집니다:
- JwtProperties의
refreshTokenExpiration필드에/** 밀리초(ms) 단위 */주석 추가 - AuthServiceImpl 115줄의
/1000위에// 밀리초를 초 단위로 변환주석 추가 - 또는 명시적인 단위 변환 메서드(예:
Duration.ofMillis(ms).toSeconds()) 사용 검토
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/domain/user/service/AuthServiceImpl.java
around lines 114-115, the expiry calculation uses
jwtTokenProvider.getRefreshTokenExpiration()/1000 which is correct but unclear
about units; update to make intent explicit by either adding a comment above the
division: "// 밀리초를 초 단위로 변환", or better replace the expression with an explicit
conversion using Duration (e.g.,
Duration.ofMillis(jwtTokenProvider.getRefreshTokenExpiration()).toSeconds()),
and also add a Javadoc/comment on JwtProperties.refreshTokenExpiration
indicating it is in milliseconds (/** 밀리초(ms) 단위 */) so the unit is clear across
the codebase.
| public String getEmailFromToken(String token) { | ||
| Claims claims = Jwts.parser() | ||
| .verifyWith(getSigningKey()) | ||
| .build() | ||
| .parseSignedClaims(token) | ||
| .getPayload(); | ||
|
|
||
| return claims.getSubject(); | ||
| } |
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 | 🟠 Major
null 체크를 추가하세요.
token 파라미터에 대한 null 체크가 없습니다. null이 전달되면 예외가 발생할 수 있습니다.
다음 diff를 적용하세요:
public String getEmailFromToken(String token) {
+ if (token == null || token.trim().isEmpty()) {
+ throw new IllegalArgumentException("Token cannot be null or empty");
+ }
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}validateToken() 및 토큰 생성 메서드에도 동일한 검증을 추가하는 것이 좋습니다.
🤖 Prompt for AI Agents
In src/main/java/com/example/triptalk/global/security/jwt/JwtTokenProvider.java
around lines 52 to 60, add a null/empty check for the token parameter before
parsing: if token is null or blank, throw an IllegalArgumentException (or a
project-specific auth exception) with a clear message instead of attempting to
parse; then proceed to parse when non-empty. Also apply the same null/empty
validation to validateToken() and any token-creation methods so they
consistently reject or handle null/empty tokens.
#️⃣ 연관된 이슈
📝 작업 내용
📌 공유 사항
✅ 체크리스트
스크린샷 (선택)
💬 리뷰 요구사항 (선택)
Summary by CodeRabbit
Release Notes
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.