Conversation
- JWT 커스텀 필터 추가 - JWT 유틸리티 작성
- 테스트 컨트롤러 추가 - Entity 개발
WalkthroughSpring Security와 JWT 기반 인증 인프라(토큰 생성/검증·필터·설정), 회원가입·로그인·토큰 리프레시 API 및 사용자 도메인(JPA 엔티티·리포지토리), JPA 감사·애플리케이션 시작 로깅과 관련 빌드·설정(의존성·프로퍼티·.gitignore) 항목이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthCtrl as AuthController
participant AuthSvc as AuthService
participant UserRepo as UserRepository
participant BCrypt as BCryptPasswordEncoder
participant JwtProv as JwtTokenProvider
participant Response
Client->>AuthCtrl: POST /api/v1/auth/login (email,password)
AuthCtrl->>AuthSvc: login(request)
AuthSvc->>UserRepo: findByEmail(email)
alt 사용자 없음
AuthSvc-->>AuthCtrl: 예외(사용자 없음)
else 사용자 존재
AuthSvc->>BCrypt: matches(plain, encoded)
alt 비밀번호 불일치
AuthSvc-->>AuthCtrl: 예외(비밀번호 불일치)
else 인증 성공
AuthSvc->>JwtProv: createAccessToken(email, role)
JwtProv-->>AuthSvc: accessToken
AuthSvc->>JwtProv: createRefreshToken(email)
JwtProv-->>AuthSvc: refreshToken
AuthSvc->>JwtProv: getAccessTokenExpiresIn()
JwtProv-->>AuthSvc: expiresIn
AuthSvc-->>AuthCtrl: LoginResponse(accessToken, refreshToken, expiresIn)
end
end
AuthCtrl->>Response: 200 OK + accessToken, set HttpOnly Secure refresh cookie
Response-->>Client: tokens + user info
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (17)
.gitignore (1)
750-751: 로컬 프로파일 설정 파일 ignore 방향 좋습니다
application-local.yml을 Git에서 제외해 두신 건 로컬용 DB/JWT 설정 등 민감한 값을 보호하는 데 적절합니다. 필요하다면 팀원 온보딩을 위해application-local.example.yml같은 샘플 파일을 별도로 두는 것도 고려해 보셔도 좋겠습니다.build.gradle (1)
39-45: Security / JWT / H2 의존성 추가 방향은 타당하지만 좌표 확인을 권장합니다
spring-boot-starter-security,spring-security-test추가는 이번 PR의 인증/인가 구현과 잘 맞습니다.io.jsonwebtoken:jjwt:0.12.6는 버전과 모듈 구성이 종종 바뀌는 라이브러리라, 현재 사용 중인 예제/공식 문서 기준으로 단일jjwt아티팩트만으로 충분한지(또는jjwt-api,jjwt-impl,jjwt-jackson등 별도 모듈이 필요한지) 한 번만 더 확인해 주세요.com.h2database:h2를runtimeOnly로 두면 운영 classpath에도 포함됩니다. 실제 운영에서는 MySQL만 쓰고 H2는 로컬/테스트 전용이라면, 프로파일별 설정이나 test 전용 스코프(testRuntimeOnly)로 한정하는 것도 고려해 볼 수 있습니다.현재 Spring Boot / JJWT 최신 문서를 한 번 검색해서, 이 버전 조합에서 권장하는 의존성 구성이 무엇인지 재확인해 주세요.
src/main/java/team/wego/wegobackend/auth/application/dto/request/LoginRequest.java (1)
1-17: 로그인 요청 DTO 설계 무난합니다이메일/비밀번호에 대한
@NotBlank조합과 한글 메시지가 회원가입 DTO와 일관적이라, 현재 요구사항 기준으로는 충분해 보입니다. 이후 비밀번호 정책이 정해지면@Size나@Pattern을 추가하는 정도만 고려하시면 될 것 같습니다.src/main/java/team/wego/wegobackend/auth/application/dto/request/SignupRequest.java (1)
13-25: 회원가입 DTO 유효성 정의 일관적이며, 닉네임 메시지 문구만 소소한 보완 여지
- 이메일/비밀번호/닉네임/전화번호에 대한 제약과 한글 메시지가 잘 정의되어 있고, 로그인 DTO와도 일관성이 있습니다.
- 다만
nickName필드에"이름은 필수입니다"라는 메시지를 사용하고 있어, 실제 UI에서는"닉네임은 필수입니다"처럼 필드명과 용어를 맞춰 주면 사용자가 헷갈지 않을 것 같습니다.src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.java (1)
139-142:maskPassword에서 URL null 처리로 방어적 코드를 추가하면 좋습니다일부
DataSource구현에서metaData.getURL()이null을 반환할 수도 있어, 현재 구현은 이 경우 NPE가 발생합니다(물론 상위에서 catch 하고 있긴 합니다). 아래처럼 간단한 null 처리를 추가하면 로그 실패 자체도 방지할 수 있습니다.- private String maskPassword(String url) { - // URL에서 비밀번호 마스킹 - return url.replaceAll("password=[^&;]+", "password=****"); - } + private String maskPassword(String url) { + if (url == null) { + return null; + } + // URL에서 비밀번호 마스킹 + return url.replaceAll("password=[^&;]+", "password=****"); + }src/main/resources/application.yml (1)
5-7: 프로파일/DDL/JWT 설정 전반은 합리적이나, 활성 프로파일 하드코딩은 한 번 더 검토를 권장합니다
spring.jpa.hibernate.ddl-auto: none으로 바꾸신 것은 운영/공용 DB에서 스키마를 임의로 생성/드롭하지 않도록 막는 측면에서 좋은 선택입니다(마이그레이션 도구나 별도 스크립트와 함께 사용 가정으로 보입니다).jwt.secret과 access/refresh 토큰 만료 설정 구조(jwt.access.token.expiration,jwt.refresh.token.expiration)도 분리도가 좋고,ApplicationStartupLogger에서 이 키들을 사용하도록만 맞춰 주시면 일관성이 유지됩니다(별도 코멘트에서 제안드린 대로).- 다만
application.yml에spring.profiles.active: local을 직접 넣어 두면, 별도 환경 변수/옵션으로 덮어쓰지 않는 한 모든 환경에서local프로파일이 기본 활성됩니다. 실제 배포 환경에서prod/stage등의 프로파일을 사용할 계획이라면:
- 배포 환경에서
SPRING_PROFILES_ACTIVE로 항상 덮어쓰거나,- 또는
application-local.yml에만spring.config.activate.on-profile: local형태로 로컬 전용 설정을 두고, 기본application.yml에는active를 비워 두는 방식을 고려해 보셔도 좋겠습니다.배포 방식에 따라 현재
spring.profiles.active설정이 의도한 대로 동작하는지(예: Docker/K8s에서 env로 항상 override하는지) 한 번만 더 확인해 주세요.Also applies to: 37-37, 81-90
src/main/java/team/wego/wegobackend/auth/application/dto/response/SignupResponse.java (1)
19-27: PII 노출 검토 필요회원가입 응답에
phoneNumber같은 민감한 개인정보(PII)가 포함되어 있습니다. 이 정보가 클라이언트에 반드시 필요한지 검토해 주세요. 특히phoneNumber는 회원가입 확인 응답에서 반환할 필요가 없을 수 있습니다.최소 권한 원칙에 따라 필요한 정보만 반환하는 것을 권장합니다.
src/main/java/team/wego/wegobackend/user/presentation/UserController.java (2)
9-13: 사용되지 않는 어노테이션 정리
@Slf4j와@RequiredArgsConstructor어노테이션이 선언되어 있지만 현재 사용되지 않습니다. 테스트 목적의 임시 컨트롤러라면 불필요한 어노테이션을 제거하거나, 향후 기능 확장 시 필요하다면 그대로 유지해도 됩니다.
15-19: 테스트 엔드포인트 명시 필요이 엔드포인트가 인증/인가 테스트 목적이라면 주석이나 TODO로 명시하여 프로덕션 배포 전 제거 또는 비활성화가 필요함을 표시하는 것이 좋습니다.
+ // TODO: 테스트용 엔드포인트 - 프로덕션 배포 전 제거 필요 @GetMapping("/test") public String test() { - return "test Success"; }src/main/java/team/wego/wegobackend/common/security/SecurityConfig.java (1)
55-55: CORS 설정 추가 필요PR 목표에 명시된 대로 CORS 설정이 필요합니다. 프론트엔드와의 통합 전에 반드시 구현되어야 합니다.
CORS 설정 구현을 도와드릴까요?
src/main/java/team/wego/wegobackend/common/security/CustomUserDetailsService.java (1)
21-24: 예외 메시지에서 이메일 노출 주의예외 메시지에 사용자 이메일이 포함되어 있습니다. 이 정보가 로그에 기록될 경우 민감한 정보가 노출될 수 있습니다.
User user = userRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email)); + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다"));로깅이 필요한 경우 별도로 마스킹 처리하여 기록하는 것을 권장합니다.
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java (1)
17-44: AuthController 설계는 깔끔하며 현재 요구사항을 잘 충족합니다
- 엔드포인트 URL, HTTP 메서드,
@Valid @RequestBody적용 등 전반적인 구성은 무난합니다.- 향후 PR 설명에 있는 “공통 응답 / 커스텀 예외 처리”가 도입되면, 컨트롤러에서도
ResponseEntity.ok(...)대신 공통 응답 포맷을 반환하도록 맞춰주는 정도만 추가로 고려하시면 될 것 같습니다.@Slf4j를 이미 붙이셨으니, 로그인/회원가입 성공·실패 시 간단한 로그 정도만 남겨 두면 추후 운영 시 추적에 도움이 될 것 같습니다.src/main/java/team/wego/wegobackend/common/security/SecurityEndpoints.java (1)
3-10: 유틸리티 클래스는 인스턴스화 불가하게 막아두면 좋습니다
SecurityEndpoints는 상수만 가지고 있어 인스턴스화될 필요가 없어 보입니다. final 클래스 + private 생성자를 두면 의도를 더 명확히 할 수 있습니다.-public class SecurityEndpoints { - - public static final String[] PUBLIC_PATTERNS = { - "/api/v*/auth/**", - "/api/v*/docs/**", - "/api/v*/health", - "/error" - }; -} +public final class SecurityEndpoints { + + private SecurityEndpoints() { + throw new AssertionError("Cannot instantiate utility class"); + } + + public static final String[] PUBLIC_PATTERNS = { + "/api/v*/auth/**", + "/api/v*/docs/**", + "/api/v*/health", + "/error" + }; +}src/main/java/team/wego/wegobackend/user/domain/User.java (1)
55-68: Boolean 래퍼 대신 primitiveboolean사용을 검토해 주세요
notification_enabled,is_deleted컬럼이nullable = false인데, 자바 타입은Boolean(래퍼)라 이론상 null이 들어갈 여지가 있습니다.- 현재는 필드 초기값을
false로 주고 있어 실질적인 문제는 없지만, 도메인 의미상 “반드시 true/false 둘 중 하나”여야 하므로 primitiveboolean으로 두는 편이 더 안전합니다.예시:
- @Column(name = "notification_enabled", nullable = false) - private Boolean notificationEnabled = false; + @Column(name = "notification_enabled", nullable = false) + private boolean notificationEnabled = false; @@ - @Column(name = "is_deleted", nullable = false) - private Boolean deleted = false; + @Column(name = "is_deleted", nullable = false) + private boolean deleted = false;src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java (1)
18-42: LoginResponse 구성은 일관적이며 사용성도 좋습니다 (만료 시간 표현 관련 소소한 제안)
- 액세스/리프레시 토큰,
tokenType = "Bearer",expiresIn,user포함 구조가 직관적이고, 정적 팩토리 메서드 패턴도 잘 맞습니다.expiresAt를LocalDateTime.now().plusSeconds(expiresIn)으로 계산하는 것도 실사용에는 충분하지만, 추후 타임존 이슈나 서버 간 시간 차이를 더 엄밀히 다루고 싶다면Instant혹은OffsetDateTime기반으로 표현하는 것도 한 번 고민해 볼 만합니다.src/main/java/team/wego/wegobackend/auth/application/AuthService.java (1)
29-75: 회원가입/로그인 핵심 로직은 요구사항을 잘 충족하며, 예외/중복 처리에서 몇 가지 다듬을 부분이 있습니다
- 회원가입에서 이메일 중복은 사전 체크하고 있지만 닉네임은 DB
unique제약에만 의존하는 구조라, 중복 시에는 현재로서는 다소 애매한 서버 에러가 나갈 수 있습니다. 추후 커스텀 예외/공통 응답을 붙이실 때 닉네임 중복도 도메인 예외로 변환해 주면 사용자 경험이 더 좋아질 것 같습니다.- 로그인에서 “존재하지 않는 사용자”와 “잘못된 비밀번호”를 구분해 주는 것은 친절하지만, 계정 존재 여부를 유추할 수 있어 보안 측면에서는 약점이 될 수 있습니다. 나중에 예외 계층을 정리하실 때 두 경우를 동일한 메시지/응답 코드(예: “이메일 또는 비밀번호가 올바르지 않습니다”)로 합치는 것도 고려해 보시면 좋겠습니다.
- 그 외 비즈니스 플로우(패스워드 인코딩, 삭제 계정 차단, JWT 발급 및 만료시간 계산)는 현재 PR 범위 내에서는 자연스럽게 잘 구성되어 있습니다.
src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java (1)
21-35: JWT 시크릿 초기화 시 설정 검증을 명시적으로 추가하는 것을 권장합니다.
secretKeyString이null이거나 공백이거나 Base64 형식이 아니면Decoders.BASE64.decode(secretKeyString)에서 애매한 예외로 애플리케이션이 부팅 중 실패합니다. 지금처럼 fail-fast 하는 방향은 좋지만, 설정 누락/오타에 대해 더 명확한 메시지로IllegalStateException을 던지면 운영 중 트러블슈팅이 훨씬 수월할 것 같습니다.또한
ApplicationStartupLogger.logJwtSettings()에서는jwt.expiration값을 로그로 출력하는데, 이 클래스는jwt.access.token.expiration,jwt.refresh.token.expiration을 사용 중이라 설정 키가 서로 달라 혼동을 줄 수 있습니다. 가능하다면 하나의 규칙으로 통일하거나, 로그 쪽도 access/refresh 각각을 출력하도록 맞추는 것을 고려해 주세요.예시:
@PostConstruct protected void init() { - this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKeyString)); + if (secretKeyString == null || secretKeyString.isBlank()) { + throw new IllegalStateException("jwt.secret 가 설정되어 있지 않습니다."); + } + + this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKeyString)); }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
.gitignore(1 hunks)build.gradle(1 hunks)src/main/java/team/wego/wegobackend/WegobackendApplication.java(1 hunks)src/main/java/team/wego/wegobackend/auth/application/AuthService.java(1 hunks)src/main/java/team/wego/wegobackend/auth/application/dto/request/LoginRequest.java(1 hunks)src/main/java/team/wego/wegobackend/auth/application/dto/request/SignupRequest.java(1 hunks)src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java(1 hunks)src/main/java/team/wego/wegobackend/auth/application/dto/response/SignupResponse.java(1 hunks)src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java(1 hunks)src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.java(1 hunks)src/main/java/team/wego/wegobackend/common/entity/BaseTimeEntity.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/CustomUserDetailsService.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/Role.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/SecurityConfig.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/SecurityEndpoints.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java(1 hunks)src/main/java/team/wego/wegobackend/user/application/dto/response/UserInfoResponse.java(1 hunks)src/main/java/team/wego/wegobackend/user/domain/User.java(1 hunks)src/main/java/team/wego/wegobackend/user/presentation/UserController.java(1 hunks)src/main/java/team/wego/wegobackend/user/repository/UserRepository.java(1 hunks)src/main/resources/application.yml(3 hunks)src/test/http/auth/auth-api.http(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
src/main/java/team/wego/wegobackend/user/presentation/UserController.java (1)
src/main/java/team/wego/wegobackend/image/presentation/ImageController.java (1)
RestController(17-94)
src/main/java/team/wego/wegobackend/auth/application/dto/request/LoginRequest.java (3)
src/main/java/team/wego/wegobackend/auth/application/dto/request/SignupRequest.java (1)
Getter(9-26)src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java (1)
Getter(13-43)src/main/java/team/wego/wegobackend/auth/application/dto/response/SignupResponse.java (1)
Getter(12-38)
src/main/java/team/wego/wegobackend/auth/application/dto/response/SignupResponse.java (3)
src/main/java/team/wego/wegobackend/auth/application/dto/request/LoginRequest.java (1)
Getter(8-18)src/main/java/team/wego/wegobackend/auth/application/dto/request/SignupRequest.java (1)
Getter(9-26)src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java (1)
Getter(13-43)
src/main/java/team/wego/wegobackend/user/domain/User.java (1)
src/main/java/team/wego/wegobackend/user/application/dto/response/UserInfoResponse.java (1)
Getter(12-55)
src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java (2)
src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java (1)
Slf4j(19-67)src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.java (1)
Component(14-143)
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java (1)
src/main/java/team/wego/wegobackend/auth/application/AuthService.java (1)
Slf4j(17-76)
src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java (1)
src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java (1)
Slf4j(17-109)
src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java (3)
src/main/java/team/wego/wegobackend/auth/application/dto/request/LoginRequest.java (1)
Getter(8-18)src/main/java/team/wego/wegobackend/auth/application/dto/request/SignupRequest.java (1)
Getter(9-26)src/main/java/team/wego/wegobackend/auth/application/dto/response/SignupResponse.java (1)
Getter(12-38)
src/main/java/team/wego/wegobackend/auth/application/dto/request/SignupRequest.java (1)
src/main/java/team/wego/wegobackend/auth/application/dto/request/LoginRequest.java (1)
Getter(8-18)
src/main/java/team/wego/wegobackend/common/security/SecurityConfig.java (1)
src/main/java/team/wego/wegobackend/common/security/SecurityEndpoints.java (1)
SecurityEndpoints(3-11)
🔇 Additional comments (10)
src/main/java/team/wego/wegobackend/WegobackendApplication.java (1)
5-8: JPA Auditing 활성화 구성 적절합니다
@EnableJpaAuditing을 메인 애플리케이션에 추가한 방향이BaseTimeEntity기반 생성/수정일 관리와 잘 맞습니다. 현재 구조에서는 이 위치에 두셔도 무방해 보입니다.src/main/java/team/wego/wegobackend/common/security/Role.java (1)
1-6: 역할 Enum 정의가 Spring Security 컨벤션과 잘 맞습니다
ROLE_USER,ROLE_ADMIN처럼ROLE_프리픽스를 포함한 Enum 설계는SimpleGrantedAuthority(user.getRole().name())패턴과 잘 호환되고, 이후 역할 확장도 용이해 보입니다.src/main/java/team/wego/wegobackend/auth/application/dto/response/SignupResponse.java (1)
29-37: LGTM!정적 팩토리 메서드 패턴이 잘 적용되어 있으며,
LoginResponse와 일관된 구조를 유지하고 있습니다.src/test/http/auth/auth-api.http (1)
1-27: LGTM! 인증 플로우 테스트가 잘 구성되어 있습니다.회원가입 → 로그인 → 토큰 추출 → 보호된 API 호출의 전체 플로우가 잘 작성되어 있습니다. 추후 에러 케이스(잘못된 자격 증명, 중복 이메일, 만료된 토큰 등)에 대한 테스트도 추가하면 더욱 완성도 높은 테스트가 될 것입니다.
src/main/java/team/wego/wegobackend/common/security/SecurityConfig.java (2)
38-43: CSRF 비활성화 확인Stateless JWT 기반 API에서 CSRF 비활성화는 적절합니다. 다만, 향후 쿠키 기반 인증(Refresh Token 등)을 도입할 경우 CSRF 보호 재검토가 필요합니다.
49-50: JWT 필터 설정 적절함
UsernamePasswordAuthenticationFilter앞에 JWT 필터를 배치하여 토큰 기반 인증이 먼저 처리되도록 올바르게 구성되었습니다.src/main/java/team/wego/wegobackend/common/security/CustomUserDetailsService.java (1)
25-31: LGTM!Spring Security의
UserDetails빌더를 올바르게 사용하고 있으며,ROLE_접두사가 적절히 적용되어 있습니다.src/main/java/team/wego/wegobackend/user/repository/UserRepository.java (1)
7-12: UserRepository interface follows Spring Data JPA conventions effectively.The naming of
existsByEmail()andfindByEmail()is idiomatic and will be auto-implemented by Spring Data JPA. Theunique = true(line 30 of User.java), which ensures data integrity and makes these query methods efficient for authentication and duplicate-check operations.src/main/java/team/wego/wegobackend/common/entity/BaseTimeEntity.java (1)
13-24: 감사(Auditing) 기본 엔티티 구성이 표준적입니다
@MappedSuperclass+AuditingEntityListener+@CreatedDate/@LastModifiedDate패턴이 잘 적용되어 있고,createdAt의updatable = false설정도 적절해 보입니다.User등 도메인 엔티티에서 상속해 사용하는 용도에 무리가 없어 보입니다.src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java (1)
40-63: 액세스/리프레시 토큰 생성 구조는 명확하고 확장 가능해 보입니다.
subject에 이메일을 두고,type으로 access/refresh 를 구분하며, role 은 선택적으로 claim 에 넣는 구조가 깔끔합니다.createToken으로 공통 로직을 모아둔 것도 좋고, HS256 +SecretKey사용도 jjwt 최신 API 사용 방식과 잘 맞습니다. 이 부분은 현재 형태 그대로 사용해도 무방해 보입니다.
| private void logJwtSettings() { | ||
| log.info("🔐 JWT SETTINGS:"); | ||
|
|
||
| String secret = env.getProperty("jwt.secret"); | ||
| if (secret != null) { | ||
| log.info(" - Secret Key: {}... (length: {})", | ||
| secret.substring(0, Math.min(10, secret.length())), | ||
| secret.length() | ||
| ); | ||
| } else { | ||
| log.warn(" - Secret Key: NOT CONFIGURED!"); | ||
| } | ||
|
|
||
| String expiration = env.getProperty("jwt.expiration"); | ||
| if (expiration != null) { | ||
| long expirationMs = Long.parseLong(expiration); | ||
| long hours = expirationMs / (1000 * 60 * 60); | ||
| log.info(" - Token Expiration: {} ms ({} hours)", expirationMs, hours); | ||
| } else { | ||
| log.warn(" - Token Expiration: NOT CONFIGURED!"); | ||
| } |
There was a problem hiding this comment.
JWT 설정 로깅에서 시크릿 노출 및 프로퍼티 키 불일치 문제가 있습니다
-
시크릿 일부 로깅 (보안 이슈)
jwt.secret의 앞 10자를 그대로 로그에 남기고 있어, 로그 유출 시 공격자에게 비밀번호 힌트를 제공하는 효과가 날 수 있습니다. 운영 환경까지 같은 로거를 사용할 것을 생각하면, 시크릿의 값 자체는 전혀 찍지 않는 편이 안전합니다(길이 정도만 노출). -
만료 시간 프로퍼티 키 불일치 (설정 오인 가능)
현재 코드는jwt.expiration을 읽지만,application.yml에는
jwt.access.token.expiration,jwt.refresh.token.expiration만 정의되어 있어 항상 “NOT CONFIGURED!”로만 찍힐 것입니다. 실제로는 설정이 되어 있는데 로그 상으로는 안 된 것처럼 보여서 디버깅 시 혼란을 줄 수 있습니다.
아래처럼 수정하면 두 문제를 한 번에 해소할 수 있습니다.
private void logJwtSettings() {
log.info("🔐 JWT SETTINGS:");
String secret = env.getProperty("jwt.secret");
- if (secret != null) {
- log.info(" - Secret Key: {}... (length: {})",
- secret.substring(0, Math.min(10, secret.length())),
- secret.length()
- );
- } else {
- log.warn(" - Secret Key: NOT CONFIGURED!");
- }
-
- String expiration = env.getProperty("jwt.expiration");
- if (expiration != null) {
- long expirationMs = Long.parseLong(expiration);
- long hours = expirationMs / (1000 * 60 * 60);
- log.info(" - Token Expiration: {} ms ({} hours)", expirationMs, hours);
- } else {
- log.warn(" - Token Expiration: NOT CONFIGURED!");
- }
+ if (secret != null && !secret.isBlank()) {
+ log.info(" - Secret Key: [CONFIGURED] (length: {})", secret.length());
+ } else {
+ log.warn(" - Secret Key: NOT CONFIGURED!");
+ }
+
+ String accessExpiration = env.getProperty("jwt.access.token.expiration");
+ if (accessExpiration != null) {
+ long expirationMs = Long.parseLong(accessExpiration);
+ long hours = expirationMs / (1000 * 60 * 60);
+ log.info(" - Access Token Expiration: {} ms ({} hours)", expirationMs, hours);
+ } else {
+ log.warn(" - Access Token Expiration: NOT CONFIGURED!");
+ }
+
+ String refreshExpiration = env.getProperty("jwt.refresh.token.expiration");
+ if (refreshExpiration != null) {
+ long expirationMs = Long.parseLong(refreshExpiration);
+ long hours = expirationMs / (1000 * 60 * 60);
+ log.info(" - Refresh Token Expiration: {} ms ({} hours)", expirationMs, hours);
+ } else {
+ log.warn(" - Refresh Token Expiration: NOT CONFIGURED!");
+ }
}이렇게 하면 시크릿 값 노출 없이, 설정된 access/refresh 만료 시간 정보를 정확히 확인할 수 있습니다.
📝 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.
| private void logJwtSettings() { | |
| log.info("🔐 JWT SETTINGS:"); | |
| String secret = env.getProperty("jwt.secret"); | |
| if (secret != null) { | |
| log.info(" - Secret Key: {}... (length: {})", | |
| secret.substring(0, Math.min(10, secret.length())), | |
| secret.length() | |
| ); | |
| } else { | |
| log.warn(" - Secret Key: NOT CONFIGURED!"); | |
| } | |
| String expiration = env.getProperty("jwt.expiration"); | |
| if (expiration != null) { | |
| long expirationMs = Long.parseLong(expiration); | |
| long hours = expirationMs / (1000 * 60 * 60); | |
| log.info(" - Token Expiration: {} ms ({} hours)", expirationMs, hours); | |
| } else { | |
| log.warn(" - Token Expiration: NOT CONFIGURED!"); | |
| } | |
| private void logJwtSettings() { | |
| log.info("🔐 JWT SETTINGS:"); | |
| String secret = env.getProperty("jwt.secret"); | |
| if (secret != null && !secret.isBlank()) { | |
| log.info(" - Secret Key: [CONFIGURED] (length: {})", secret.length()); | |
| } else { | |
| log.warn(" - Secret Key: NOT CONFIGURED!"); | |
| } | |
| String accessExpiration = env.getProperty("jwt.access.token.expiration"); | |
| if (accessExpiration != null) { | |
| long expirationMs = Long.parseLong(accessExpiration); | |
| long hours = expirationMs / (1000 * 60 * 60); | |
| log.info(" - Access Token Expiration: {} ms ({} hours)", expirationMs, hours); | |
| } else { | |
| log.warn(" - Access Token Expiration: NOT CONFIGURED!"); | |
| } | |
| String refreshExpiration = env.getProperty("jwt.refresh.token.expiration"); | |
| if (refreshExpiration != null) { | |
| long expirationMs = Long.parseLong(refreshExpiration); | |
| long hours = expirationMs / (1000 * 60 * 60); | |
| log.info(" - Refresh Token Expiration: {} ms ({} hours)", expirationMs, hours); | |
| } else { | |
| log.warn(" - Refresh Token Expiration: NOT CONFIGURED!"); | |
| } | |
| } |
src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java
Show resolved
Hide resolved
src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java
Show resolved
Hide resolved
| .authorizeHttpRequests((auth) -> auth | ||
| .requestMatchers(PathRequest.toH2Console()).permitAll() | ||
| .requestMatchers(SecurityEndpoints.PUBLIC_PATTERNS).permitAll() | ||
| .anyRequest().authenticated() | ||
| ); |
There was a problem hiding this comment.
H2 Console 접근 제한: 프로덕션 환경 고려 필요
H2 Console에 대한 permitAll() 설정이 있습니다. 개발 환경에서는 유용하지만, 프로덕션 환경에서는 보안 위험이 됩니다. 프로파일 기반으로 H2 Console 접근을 제한하는 것을 권장합니다.
+ // H2 Console은 개발 환경에서만 허용되어야 함
.requestMatchers(PathRequest.toH2Console()).permitAll()프로덕션 배포 전에 H2 Console 비활성화 또는 프로파일 분리가 필요합니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/team/wego/wegobackend/common/security/SecurityConfig.java
around lines 32-36, the H2 Console is currently exposed via
requestMatchers(PathRequest.toH2Console()).permitAll(), which is unsafe for
production; update the configuration to apply that permitAll only when a
non-production profile is active (e.g., "dev" or "test") by injecting Spring's
Environment (or using @Profile) and conditionally adding the
PathRequest.toH2Console() matcher inside the HttpSecurity configuration when the
active profiles include your dev/test profile(s); ensure the H2-specific
frameOptions/csrf relaxations (if present) are also only applied under the same
profile so the console is disabled in production.
src/main/java/team/wego/wegobackend/user/application/dto/response/UserInfoResponse.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/main/java/team/wego/wegobackend/user/application/dto/response/UserInfoResponse.java (1)
12-54: 이전 리뷰 피드백 반영 완료 및 구현 양호이전 리뷰에서 지적된
createAt오타가createdAt으로 수정되어 일관성이 확보되었습니다. DTO 구조와 빌더 패턴 사용이 적절하며, 팩토리 메서드를 통한 변환 로직도 명확합니다.선택적 개선 사항: Boolean 필드명 "is" 접두사 제거 고려
Lines 34, 36의
isNotificationEnabled,isDeleted필드명은 Java 관례상 "is" 접두사를 제거하는 것이 권장됩니다. Lombok이 자동으로 getter에 "is"나 "get"을 추가하기 때문에, 필드명 자체에 "is"가 있으면getIsDeleted()처럼 다소 어색한 메서드명이 생성됩니다.필드명을
notificationEnabled,deleted로 변경하면 더 자연스러운 getter가 생성됩니다:- private Boolean isNotificationEnabled; + private Boolean notificationEnabled; - private Boolean isDeleted; + private Boolean deleted;그리고 빌더 호출 부분도 함께 수정:
- .isNotificationEnabled(user.getNotificationEnabled()) - .isDeleted(user.getDeleted()) + .notificationEnabled(user.getNotificationEnabled()) + .deleted(user.getDeleted())src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.java (1)
100-111: JWT 만료 시간 로깅을 access/refresh 분리 및 파싱 오류 방어 로직으로 확장하면 더 안정적입니다현재는
jwt.access.token.expiration하나만 로깅하고,Long.parseLong실패 시 애플리케이션 준비 이벤트 처리 단계에서 런타임 예외가 그대로 전파될 수 있습니다. access/refresh 를 각각 로깅하고, 잘못된 설정값에 대해서는 예외 대신 에러 로그를 남기는 형태가 운영 관점에서 조금 더 안전할 것 같습니다.예시 리팩터:
- private void logJwtSettings() { - log.info("🔐 JWT SETTINGS:"); - - String expiration = env.getProperty("jwt.access.token.expiration"); - if (expiration != null) { - long expirationMs = Long.parseLong(expiration); - long hours = expirationMs / (1000 * 60 * 60); - log.info(" - Token Expiration: {} ms ({} hours)", expirationMs, hours); - } else { - log.warn(" - Token Expiration: NOT CONFIGURED!"); - } - } + private void logJwtSettings() { + log.info("🔐 JWT SETTINGS:"); + + logTokenExpiration("Access", "jwt.access.token.expiration"); + logTokenExpiration("Refresh", "jwt.refresh.token.expiration"); + } + + private void logTokenExpiration(String label, String propertyKey) { + String expiration = env.getProperty(propertyKey); + if (expiration == null || expiration.isBlank()) { + log.warn(" - {} Token Expiration: NOT CONFIGURED! (property: {})", label, propertyKey); + return; + } + try { + long expirationMs = Long.parseLong(expiration); + long hours = expirationMs / (1000 * 60 * 60); + log.info(" - {} Token Expiration: {} ms ({} hours)", label, expirationMs, hours); + } catch (NumberFormatException e) { + log.error(" - {} Token Expiration: INVALID VALUE '{}' (property: {})", label, expiration, propertyKey); + } + }이렇게 하면:
- access/refresh 토큰 만료 시간을 각각 확인할 수 있고,
- 잘못된 설정값이 있더라도 애플리케이션 시작은 유지하면서 로그로 명확히 진단할 수 있습니다.
참고로, 이전 리뷰에서 지적되었던
jwt.secret값 일부 노출 문제는 현재 코드에서는 제거된 상태라 보안 측면에서 더 안전해진 것으로 보입니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.java(1 hunks)src/main/java/team/wego/wegobackend/user/application/dto/response/UserInfoResponse.java(1 hunks)
🔇 Additional comments (1)
src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.java (1)
22-44: 스타트업 로깅 엔트리 포인트 구조가 명확하고 책임 분리가 잘 되어 있습니다
ApplicationReadyEvent리스너에서 프로필/DB/JPA/JWT/서버 정보를 각 전용 메서드로 위임하는 구조가 이해하기 쉽고, 실제 비즈니스 빈에서 진단용 로깅을 분리한 점이 좋습니다. 운영 시에도 장애 원인 파악에 유용할 것 같습니다.
| private void logDatabaseInfo() { | ||
| try (Connection connection = dataSource.getConnection()) { | ||
| DatabaseMetaData metaData = connection.getMetaData(); | ||
|
|
||
| log.info("🗄️ DATABASE INFORMATION:"); | ||
| log.info(" - Database Type: {} {}", | ||
| metaData.getDatabaseProductName(), | ||
| metaData.getDatabaseProductVersion() | ||
| ); | ||
| log.info(" - JDBC Driver: {} {}", | ||
| metaData.getDriverName(), | ||
| metaData.getDriverVersion() | ||
| ); | ||
| log.info(" - Connection URL: {}", | ||
| maskPassword(metaData.getURL()) | ||
| ); | ||
| log.info(" - Username: {}", metaData.getUserName()); | ||
| log.info(" - Max Connections: {}", | ||
| env.getProperty("spring.datasource.hikari.maximum-pool-size", "10") | ||
| ); | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("❌ Failed to retrieve database information", e); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "ApplicationStartupLogger.java" | head -5Repository: WeGo-Together/WeGo_BackEnd
Length of output: 151
🏁 Script executed:
cat -n src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 5513
🌐 Web query:
JDBC DatabaseMetaData getURL() null behavior specification
💡 Result:
The JDBC spec (DatabaseMetaData.getURL()) says the method "retrieves the URL for this DBMS" and may return the URL or null if it cannot be generated — i.e., drivers are allowed to return null when they cannot determine/produce a URL. See the DatabaseMetaData javadoc. [1][2]
Sources
- java.sql.DatabaseMetaData.getURL() javadoc (Java SE): "the URL for this DBMS or null if it cannot be generated." [1]
- Oracle JDBC DatabaseMetaData javadoc: "Returns: the url or null if it can't be generated." [2]
[1] https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/DatabaseMetaData.html
[2] https://docs.oracle.com/database/ (Oracle JDBC DatabaseMetaData getURL documentation)
🌐 Web query:
Java String replaceAll null pointer exception behavior
💡 Result:
Short answer: String.replaceAll delegates to Pattern.compile(regex).matcher(this).replaceAll(repl). So:
- If the regex argument is null you get a NullPointerException (Pattern.compile(null) → NPE). [1][2]
- If you call replaceAll on a null String reference you'll also get a NullPointerException (calling any instance method on null). [1]
- If the replacement argument is null the Matcher.replaceAll/replaceFirst implementation throws a NullPointerException. [2][3]
References:
- java.lang.String.replaceAll javadoc / description. [Oracle Java docs]. [1]
- java.util.regex.Pattern / Matcher behavior (Pattern.compile and Matcher.replaceAll). [Oracle Java docs]. [2]
- Matcher.replaceFirst source / docs showing explicit NPE on null replacement. [3]
(If you want, I can paste minimal example code demonstrating each case.)
🌐 Web query:
JDBC URL password parameter naming conventions case sensitivity
💡 Result:
Short answer: JDBC URL parameter names (including the password key) are driver-defined and not guaranteed to be treated the same across all drivers — their spelling and case-sensitivity depend on the driver’s parser. Common conventions are user/username and password or pwd, but you must check the driver docs for the exact accepted key(s) and whether the driver normalizes case.
Key points and sources:
- Driver-specific: each JDBC driver documents the accepted URL/property names (e.g., PostgreSQL lists PASSWORD / PGProperty.PASSWORD; the driver code/Docs show which keys it accepts). [1]
- Java Properties itself uses case‑sensitive String keys, so if a driver reads a java.util.Properties object directly the exact key case matters unless the driver normalizes it. [2]
- Oracle’s JDBC docs show credentials may be provided via Properties or getConnection arguments; the server’s password case-sensitivity is a separate concern (database password matching can be case‑sensitive or configurable). [3][4]
Recommendation: consult the specific JDBC driver documentation (and/or inspect the driver’s PGProperty/constant names or source) to know the exact key names and whether the driver lowercases/normalizes parameter names. Examples: PostgreSQL PGProperty docs and Oracle JDBC docs. [1][3]
Sources:
- PostgreSQL JDBC PGProperty documentation (lists PASSWORD/PGProperty.PASSWORD). [https://jdbc.postgresql.org/documentation/publicapi/org/postgresql/PGProperty.html] [1]
- Java/Properties keys are case‑sensitive (Java design/FAQ discussion about case-sensitive property keys). [https://docs.oracle.com/en/java/javase/24/core/design-faq.html] [2]
- Oracle JDBC client/login docs (credentials via Properties or getConnection; server-side password case rules). [https://docs.oracle.com/en/database/oracle/oracle-database/26/jjdbc/client-side-security.html] [3]
- Oracle Database authentication: SEC_CASE_SENSITIVE_LOGON (password case-sensitivity controlled by DB setting). [https://docs.oracle.com/cd/E15586_01/network.1111/e16543/authentication.htm] [4]
Add null check to maskPassword() method to prevent NPE when DatabaseMetaData.getURL() returns null
Some JDBC drivers return null from DatabaseMetaData.getURL(), and calling url.replaceAll() on null will throw a NullPointerException. This should be handled defensively in the logging helper:
private String maskPassword(String url) {
- // URL에서 비밀번호 마스킹
- return url.replaceAll("password=[^&;]+", "password=****");
+ if (url == null || url.isBlank()) {
+ return "N/A";
+ }
+ // URL에서 비밀번호 마스킹
+ return url.replaceAll("password=[^&;]+", "password=****");
}This ensures database information is logged safely regardless of whether the driver provides a URL.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/common/config/ApplicationStartupLogger.java
around lines 58 to 82, guard against a potential NPE when metaData.getURL() can
be null by making maskPassword null-safe: check if the URL is null (or blank)
and return a safe placeholder such as "N/A" (or an empty string) instead of
calling replaceAll on it; update the logDatabaseInfo call to pass the result of
the null-safe maskPassword unchanged so logging will never attempt to operate on
a null URL.
|
아래 작업 추가 진행하고 있습니다.
|
- API 인가 refresh 분기처리 추가 - accesstoken 재발급 엔드포인트 개발 - Refresh Token JSON -> Cookie 변경 (secure, httponly 설정) - .http 테스트 코드 작성
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/main/java/team/wego/wegobackend/auth/application/dto/response/RefreshResponse.java (1)
10-32: LoginResponse와 동일한 패턴이 반복됩니다.이 응답 구조는
LoginResponse와 매우 유사한 토큰 응답 패턴을 공유합니다.앞서
LoginResponse.java리뷰에서 언급한 것처럼, 공통 부분을 추출하여 코드 중복을 줄이는 것을 권장합니다.
🧹 Nitpick comments (5)
src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java (1)
14-44: 구조는 양호하나 RefreshResponse와 코드 중복이 있습니다.DTO 구조와
@JsonIgnore를 통한 refreshToken 보안 처리는 적절합니다. 다만RefreshResponse와 동일한 패턴(tokenType="Bearer", expiresAt 계산 로직)이 반복되고 있습니다.향후 유지보수성 향상을 위해 공통 토큰 응답 필드를 base class나 composition으로 추출하는 것을 고려해보세요:
@Getter @SuperBuilder public abstract class TokenResponse { private String tokenType = "Bearer"; private Long expiresIn; private LocalDateTime expiresAt; protected static LocalDateTime calculateExpiresAt(Long expiresIn) { return LocalDateTime.now().plusSeconds(expiresIn); } }src/main/java/team/wego/wegobackend/auth/application/AuthService.java (1)
65-67: 탈퇴 계정 검증 로직이 중복됩니다.
login과refresh메서드 모두에서 동일한 탈퇴 계정 체크가 반복됩니다.공통 검증 메서드로 추출하세요:
private void validateActiveUser(User user) { if (user.getDeleted()) { throw new IllegalArgumentException("탈퇴한 계정"); } }그리고 두 곳에서
validateActiveUser(user);로 호출하세요.Also applies to: 93-95
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java (2)
57-68: Refresh 엔드포인트 구현이 적절하나 간소화 가능합니다.쿠키 검증 로직이 올바르게 동작합니다.
required=false와 수동 null 체크 대신 더 간결하게 처리할 수 있습니다:@PostMapping("/refresh") public ResponseEntity<RefreshResponse> refresh( @CookieValue(name = "refreshToken") String refreshToken ) { RefreshResponse response = authService.refresh(refreshToken); return ResponseEntity.ok(response); }Spring이 쿠키가 없을 때 자동으로 400 에러를 반환하며, 커스텀 예외 핸들러에서 처리할 수 있습니다.
73-81: 쿠키 설정이 보안 요구사항을 충족합니다.HttpOnly, Secure, SameSite 설정이 모두 적용되어 XSS 및 CSRF 공격에 대한 방어가 구현되었습니다.
Spring 6+를 사용한다면
ResponseCookie를 고려해보세요:private ResponseCookie createRefreshTokenCookie(String refreshToken) { return ResponseCookie.from("refreshToken", refreshToken) .httpOnly(true) .secure(true) .path("/") .maxAge(jwtTokenProvider.getRefreshTokenExpiration()) .sameSite("Strict") .build(); }그리고
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());로 추가합니다.src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java (1)
117-154: 이전 보안 취약점이 완전히 해결되었습니다.
validateAccessToken과validateRefreshToken메서드가 토큰 타입을 명시적으로 검증하여, 이전 리뷰에서 지적된 refresh 토큰이 access 토큰처럼 사용될 수 있는 critical 보안 이슈를 차단합니다.에러 로깅 개선을 위해 예외 세부사항을 포함하세요:
} catch (Exception e) { - log.error("Error validating access token type"); + log.error("Error validating access token type", e); return false; }Lines 130과 151에 동일하게 적용하세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/main/java/team/wego/wegobackend/auth/application/AuthService.java(1 hunks)src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java(1 hunks)src/main/java/team/wego/wegobackend/auth/application/dto/response/RefreshResponse.java(1 hunks)src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java(1 hunks)src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java(1 hunks)src/test/http/auth/auth-api.http(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/team/wego/wegobackend/auth/application/AuthService.java (1)
src/main/java/team/wego/wegobackend/common/security/CustomUserDetailsService.java (1)
Service(14-33)
src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java (4)
src/main/java/team/wego/wegobackend/auth/application/dto/response/RefreshResponse.java (1)
Getter(10-33)src/main/java/team/wego/wegobackend/auth/application/dto/request/LoginRequest.java (1)
Getter(8-18)src/main/java/team/wego/wegobackend/auth/application/dto/request/SignupRequest.java (1)
Getter(9-26)src/main/java/team/wego/wegobackend/auth/application/dto/response/SignupResponse.java (1)
Getter(12-38)
src/main/java/team/wego/wegobackend/auth/application/dto/response/RefreshResponse.java (1)
src/main/java/team/wego/wegobackend/auth/application/dto/response/LoginResponse.java (1)
Getter(14-45)
🪛 ast-grep (0.40.0)
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java
[warning] 48-48: 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(createRefreshTokenCookie(loginResponse.getRefreshToken()));
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 48-48: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(createRefreshTokenCookie(loginResponse.getRefreshToken()));
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly
(cookie-missing-httponly-java)
🔇 Additional comments (6)
src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java (1)
27-58: 이전 보안 이슈가 적절히 해결되었습니다.
validateAccessToken메서드를 사용하여 access 토큰만 인증에 사용되도록 보장합니다. 이는 이전 리뷰에서 지적된 refresh 토큰 오용 가능성을 차단합니다.필터 로직과 예외 처리가 적절하며, SecurityContext 설정도 올바르게 구현되었습니다.
src/main/java/team/wego/wegobackend/auth/application/AuthService.java (1)
36-51: 회원가입 로직이 올바르게 구현되었습니다.이메일 중복 체크, 비밀번호 암호화, 기본 역할 설정이 적절합니다.
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java (2)
35-39: 회원가입 엔드포인트가 올바르게 구현되었습니다.요청 검증과 서비스 계층 위임이 적절합니다.
44-52: 로그인 엔드포인트와 쿠키 처리가 적절합니다.Refresh token을 HttpOnly, Secure, SameSite=Strict 쿠키로 설정하는 것은 보안 모범 사례입니다. 정적 분석 경고는 **오탐(false positive)**입니다 - 코드가 이미 HttpOnly(line 75)와 SameSite(line 79)를 설정하고 있습니다.
src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java (2)
32-35: 시크릿 키 초기화가 올바르게 구현되었습니다.
@PostConstruct를 사용한 지연 초기화와 Base64 디코딩이 적절합니다.
40-63: 토큰 생성 로직이 보안 요구사항을 충족합니다.Access 토큰과 refresh 토큰을
type클레임으로 명확히 구분하고, role은 access 토큰에만 포함하는 설계가 적절합니다.
| public LoginResponse login(LoginRequest request) { | ||
|
|
||
| User user = userRepository.findByEmail(request.getEmail()) | ||
| .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자")); | ||
|
|
||
| if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { | ||
| throw new IllegalArgumentException("잘못된 비밀번호"); | ||
| } | ||
|
|
||
| if (user.getDeleted()) { | ||
| throw new IllegalArgumentException("탈퇴한 계정"); | ||
| } | ||
|
|
||
| String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), | ||
| user.getRole().name()); | ||
|
|
||
| String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); | ||
|
|
||
| Long expiresIn = jwtTokenProvider.getAccessTokenExpiresIn(); | ||
|
|
||
| return LoginResponse.of(user, accessToken, refreshToken, expiresIn); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
로그인 검증 로직은 정확하나 에러 처리 개선이 필요합니다.
비밀번호 검증 및 계정 상태 확인이 올바르게 구현되었습니다. 다만 모든 비즈니스 예외를 IllegalArgumentException으로 처리하는 것은 PR 목표에 명시된 대로 개선이 필요합니다.
커스텀 예외를 도입하여 에러 케이스를 명확히 구분하세요:
// 예시
throw new UserNotFoundException("존재하지 않는 사용자");
throw new InvalidPasswordException("잘못된 비밀번호");
throw new DeletedAccountException("탈퇴한 계정");이는 PR 설명의 TO-DO 항목("커스텀 예외 처리 적용")과 일치합니다.
🤖 Prompt for AI Agents
In src/main/java/team/wego/wegobackend/auth/application/AuthService.java around
lines 56 to 77, replace the three IllegalArgumentException usages with
domain-specific custom exceptions to improve error clarity: throw new
UserNotFoundException("존재하지 않는 사용자") when the user is not found, throw new
InvalidPasswordException("잘못된 비밀번호") when the password check fails, and throw
new DeletedAccountException("탈퇴한 계정") when the account is marked deleted; create
these exception classes (e.g., extending RuntimeException or a common
BusinessException) in an appropriate package, add the necessary imports, and
ensure existing callers/tests are updated to expect the new exception types if
necessary.
| ### 액세스 토큰 재발급 | ||
| POST http://localhost:8080/api/v1/auth/refresh | ||
| Content-Type: application/json |
There was a problem hiding this comment.
Refresh 엔드포인트 테스트가 불완전합니다.
현재 refresh 요청은 본문이 비어 있지만, AuthController의 /refresh 엔드포인트는 refreshToken 쿠키를 요구합니다. 로그인 응답에서 설정된 쿠키가 자동으로 전달되지 않으면 이 테스트는 실패합니다.
HTTP 클라이언트가 쿠키를 자동으로 관리하지 않는 경우, 다음과 같이 명시적으로 쿠키를 전달해야 합니다:
### 액세스 토큰 재발급
POST http://localhost:8080/api/v1/auth/refresh
Content-Type: application/json
+Cookie: refreshToken={{refreshToken}}또는 로그인 응답에서 refreshToken을 추출하여 변수로 저장하는 스크립트를 추가할 수 있습니다.
📝 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.
| ### 액세스 토큰 재발급 | |
| POST http://localhost:8080/api/v1/auth/refresh | |
| Content-Type: application/json | |
| ### 액세스 토큰 재발급 | |
| POST http://localhost:8080/api/v1/auth/refresh | |
| Content-Type: application/json | |
| Cookie: refreshToken={{refreshToken}} |
🤖 Prompt for AI Agents
In src/test/http/auth/auth-api.http around lines 25-27, the refresh token test
sends an empty body but the /refresh endpoint expects a refreshToken cookie;
update the test to include the refreshToken cookie (either by adding a Cookie
header with the stored refresh token value from the prior login response, or by
extracting the refresh token from the login response and saving it to a variable
and then sending that variable as the Cookie) so the request includes the same
refreshToken cookie used by the application.
There was a problem hiding this comment.
InteliJ 에딧 사용 시 자동 세팅 됩니다. <참고>
There was a problem hiding this comment.
@Be-HinD, 알려주셔서 감사합니다! IntelliJ HTTP 클라이언트가 동일 파일 내의 요청들 간에 쿠키를 자동으로 관리한다는 점을 이해했습니다. 로그인 응답에서 설정된 refreshToken 쿠키가 자동으로 후속 요청에 포함되므로, 명시적인 Cookie 헤더가 필요하지 않네요.
이 정보를 참고하도록 하겠습니다! 🙏
✏️ Learnings added
Learnt from: Be-HinD
Repo: WeGo-Together/WeGo_BackEnd PR: 32
File: src/test/http/auth/auth-api.http:25-27
Timestamp: 2025-12-07T11:11:53.271Z
Learning: IntelliJ HTTP client (.http files) automatically manages cookies between requests in the same file. When reviewing .http test files, a refresh token endpoint test does not need an explicit Cookie header because cookies from previous requests (e.g., login) are automatically included by the IDE.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java (3)
44-52: Refresh 토큰 쿠키 설정 전반은 좋지만, 만료 시간 단위/오버플로 검증이 필요해 보입니다.
createRefreshTokenCookie에서 HttpOnly, Secure, SameSite=Strict까지 잘 설정하셔서 정석적인 보안 설정입니다. 정적 분석 도구의 HttpOnly 미설정 경고는 이 헬퍼 메서드를 고려하지 않은 false positive로 보입니다.- 다만
cookie.setMaxAge((int) jwtTokenProvider.getRefreshTokenExpiration());부분은getRefreshTokenExpiration()의 단위와 범위를 한 번 더 확인하는 게 좋겠습니다.
- ms 단위
long을 그대로 넘기면int캐스팅 시 오버플로가 날 수 있고(예: 30일 ms는Integer.MAX_VALUE초과),setMaxAge는 초 단위를 기대합니다.Duration을 반환하거나, 메서드 이름을getRefreshTokenExpirationSeconds()처럼 의미가 드러나는int반환으로 바꾸는 것도 추천합니다.Also applies to: 88-95
57-67: 로그아웃 시 삭제 쿠키에도 Secure/SameSite를 맞춰 주는 게 좋습니다.현재 삭제용 쿠키는 HttpOnly만 설정되어 있고 Secure·SameSite는 빠져 있습니다.
Cookie deleteCookie = new Cookie("refreshToken", null); deleteCookie.setPath("/"); deleteCookie.setMaxAge(0); deleteCookie.setHttpOnly(true); response.addCookie(deleteCookie);실제 동작은 대부분의 브라우저에서 문제없겠지만,
- 동일 이름의 기존 쿠키가
Secure,SameSite=Strict로 설정되어 있으므로,- 삭제 쿠키도 똑같이 보안 속성을 맞춰 두는 편이 안전하고, 정적 분석 경고도 해소됩니다.
예시:
- Cookie deleteCookie = new Cookie("refreshToken", null); - deleteCookie.setPath("/"); - deleteCookie.setMaxAge(0); - deleteCookie.setHttpOnly(true); + Cookie deleteCookie = new Cookie("refreshToken", null); + deleteCookie.setPath("/"); + deleteCookie.setMaxAge(0); + deleteCookie.setHttpOnly(true); + deleteCookie.setSecure(true); + deleteCookie.setAttribute("SameSite", "Strict");
72-83:refresh엔드포인트의 예외 타입/응답 코드를 명시적으로 처리하는 편이 좋습니다.if (refreshToken == null) { throw new IllegalArgumentException("Refresh 토큰이 없습니다."); }
- 지금 상태라면 전역 예외 핸들러가 없을 경우 500으로 떨어질 수 있어, 클라이언트 입장에서 인증 실패(401/403 혹은 400) 상황과 구분이 어렵습니다.
- 도메인에 맞는 커스텀 예외(예:
InvalidRefreshTokenException) +@ResponseStatus또는 공통 예외 핸들러에서 401/400으로 내려주는 쪽이 더 명확해 보입니다.- 이미 TODO에 “커스텀 예외 처리 적용”이 있으니, 이 부분을 우선 적용할 후보로 삼으시면 좋겠습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java(1 hunks)
🧰 Additional context used
🪛 ast-grep (0.40.0)
src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java
[warning] 63-63: 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(deleteCookie);
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] 48-48: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(createRefreshTokenCookie(loginResponse.getRefreshToken()));
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly
(cookie-missing-httponly-java)
[warning] 48-48: 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(createRefreshTokenCookie(loginResponse.getRefreshToken()));
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 63-63: 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(deleteCookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
Spring Security + JWT 관련 설정 및 로직 개발
Auth 도메인 개발(로그인, 회원가입)
User 도메인 개발(Entity, 테스트 컨트롤러)
🔍 관련 이슈
해당 PR이 해결하는 이슈가 있다면 연결해주세요.
Closes #31
🧪 테스트
변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.
1. 스프링부트 로딩시점 설정 로깅
2. 보호된 API 요청 시 (토큰 x)
3. 회원가입
4. 로그인
🚨 확인해야 할 사항 (Checklist)
PR을 제출하기 전에 아래 항목들을 확인해주세요.
🙋 기타 참고 사항
TO-DO
Summary by CodeRabbit
새로운 기능
개선 사항
테스트
기타(Chore)
✏️ Tip: You can customize this high-level summary in your review settings.