[FEAT] 카카오 소셜 로그인 연동 및 인증/인가 필터 도입#6
Conversation
- redirect 정책 설정
WalkthroughSpring Security, OAuth2(Kakao), JWT, Redis 기반 인증 흐름을 도입. 보안 설정, JWT 필터/엔트리포인트, OAuth2 사용자/성공 핸들러, 인가 요청 리졸버, 토큰 스토리지(레디스), 사용자 조회 추가, 예외/에러코드 확장, 프로퍼티/의존성/JSON/Redis 설정 및 테스트 프로파일 반영. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant B as Browser
participant S as Server (App)
participant K as Kakao OAuth2
participant R as Redis
Note over B,S: OAuth2 로그인 시작
B->>S: GET /oauth2/authorization/kakao?redirect_uri={origin}
S->>S: CustomAuthorizationRequestResolver 저장(redirect_uri in session)
S-->>B: 302 Redirect to Kakao auth
B->>K: Authorize (login/consent)
K-->>S: Callback with code
S->>K: Token exchange
K-->>S: Access token
S->>S: CustomOAuth2UserService로 사용자 조회/생성(findByKakaoId)
S->>S: JwtUtil.createAccessToken(userId)
S->>R: LoginTokenStorage.put(loginTokenKey, role, jwt, TTL=5m)
S-->>B: 302 Redirect {origin}/{path}?loginTokenKey=...
Note over B,S: 토큰 교환
B->>S: POST /auth/token {loginTokenKey}
S->>R: consume(loginTokenKey)
alt 존재
R-->>S: Entry(role, token)
S-->>B: 200 { token }
else 없음
R-->>S: null
S-->>B: 401 AUTH_INVALID_LOGIN_TOKEN_KEY
end
sequenceDiagram
autonumber
participant C as Client
participant S as Server (App)
participant F as JwtAuthenticationFilter
participant Sec as SecurityContext
C->>S: API 요청 (Authorization: Bearer xxx)
activate F
F->>F: 화이트리스트 경로 여부 확인
alt 보호 경로
F->>F: 헤더에서 토큰 추출/유효성 검증
alt 유효
F->>F: JwtUtil.getLoginUser -> userId
F->>Sec: UsernamePasswordAuthenticationToken 설정
F->>S: request attribute에 userId/token 저장
else 무효/만료
F->>S: request attribute에 exception 저장
end
end
deactivate F
S-->>C: 컨트롤러 처리 결과 (예외 시 EntryPoint가 전역 예외 처리기로 위임)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 23
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/konkuk/chacall/domain/user/domain/User.java (1)
31-33: 이메일 컬럼 길이 50 → 254 권장RFC/현실 데이터를 고려하면 50은 짧습니다. 100~254 범위를 권장합니다.
- @Column(length = 50, nullable = false, unique = true) + @Column(length = 254, nullable = false, unique = true) private String email;
🧹 Nitpick comments (37)
src/main/resources/application-dev.yml (1)
74-79: static-locations 오버라이드는 신중 권장.Spring Boot 기본 정적 경로(META-INF/resources, resources, static, public)를 모두 쓰려면
static-locations를 생략하는 편이 안전합니다. 현재 설정은 static/public만 활성화합니다.- static-locations: classpath:/static/,classpath:/public/src/main/resources/static/index.html (1)
1-1: 임시 정적 페이지는 프로덕션 노출/검색 최소화 필요dev 전용이라면 프로필로만 서빙하거나 로봇 차단 메타 추가 권장.
적용 예시:
-Hello World! +<!doctype html> +<html lang="ko"> +<head> + <meta charset="utf-8"> + <meta name="robots" content="noindex,nofollow"> + <title>ChaCall Test</title> +</head> +<body> + Hello World! +</body> +</html>src/main/java/konkuk/chacall/global/common/security/oauth2/auth/dto/AuthTokenRequest.java (1)
3-6: loginTokenKey 입력값 검증(@notblank) 추가 제안빈 문자열/누락 방지로 컨트롤러 진입 전 실패시키는 편이 안전합니다. 컨트롤러 파라미터에는 @Valid도 함께 적용해 주세요.
package konkuk.chacall.global.common.security.oauth2.auth.dto; +import jakarta.validation.constraints.NotBlank; public record AuthTokenRequest( - String loginTokenKey + @NotBlank String loginTokenKey ) { }src/main/java/konkuk/chacall/global/common/security/annotation/UserId.java (1)
8-11: @documented 추가로 API 문서/IDE 힌트 개선어노테이션 사용 의도를 노출하기 위해 @documented 권장.
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.annotation.Documented; @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) +@Documented public @interface UserId { }src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1)
13-13: HTTP 상태-도메인 매핑 재검토 제안
- JSON_PARSING_ERROR: 클라이언트 요청 바디 파싱 실패라면 400(BAD_REQUEST)이 더 적합할 수 있습니다.
- AUTH_WEB_ORIGIN_EMPTY: 서버 설정 누락에 가까워 5xx(예: 500) 고려 여지.
설계 의도에 따라 현 상태 유지도 가능하니, 전역 예외 핸들러 매핑과 함께 일관성만 확인 부탁드립니다.
Also applies to: 27-27
src/main/java/konkuk/chacall/global/common/security/oauth2/OAuth2UserDetails.java (1)
5-10: null 가능성 명시 또는 계약 문서화email/profileImage는 제공 안 될 수 있어 @nullable 표기나 Javadoc으로 계약을 명확히 해 주세요.
+import org.springframework.lang.Nullable; ... String getNickname(); - String getProfileImage(); - String getEmail(); + @Nullable String getProfileImage(); + @Nullable String getEmail();src/main/java/konkuk/chacall/global/config/RedisConfig.java (1)
21-29: ObjectMapper 일관성 적용 및 Hash 시리얼라이저 설정파일: src/main/java/konkuk/chacall/global/config/RedisConfig.java (줄 21–29)
애플리케이션 전역 ObjectMapper를 주입해 GenericJackson2JsonRedisSerializer에 사용하고, Hash key/value 시리얼라이저도 설정하세요.- public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { + public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory, + com.fasterxml.jackson.databind.ObjectMapper objectMapper) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); // 일반적인 key:value의 경우 시리얼라이저 redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + // Hash 대비 + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); return redisTemplate; }메인 용도가 문자열 토큰이면 StringRedisTemplate 사용 고려.
src/main/java/konkuk/chacall/global/common/security/constant/AuthParameters.java (1)
15-17: 하드코딩된 리다이렉트 URL은 설정값으로 외부화하는 것이 안전합니다.
/index.html,/member,/owner는 환경별(로컬/스테이징/프로덕션)로 달라질 가능성이 큽니다.application-*.yml로 외부화하고 이 enum에는 키만 두는 방식을 권장합니다.src/main/java/konkuk/chacall/global/common/security/resolver/UserIdArgumentResolver.java (2)
34-39: 요청 속성 타입 안전성 보강(클래스캐스트 방지).
(Long) userId캐스트는Integer/String등으로 저장된 경우 런타임 예외를 유발합니다. 안전하게 변환하세요.적용 예:
- Object userId = ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute(JWT_ACCESS_TOKEN_KEY.getValue()); - if (userId == null) { - throw new AuthException(AUTH_TOKEN_NOT_FOUND); - } - return (Long) userId; + Object userIdAttr = ((HttpServletRequest) webRequest.getNativeRequest()) + .getAttribute(JWT_ACCESS_TOKEN_KEY.getValue()); + if (userIdAttr == null) throw new AuthException(AUTH_TOKEN_NOT_FOUND); + if (userIdAttr instanceof Long l) return l; + if (userIdAttr instanceof Integer i) return i.longValue(); + if (userIdAttr instanceof String s) return Long.parseLong(s); + throw new AuthException(AUTH_TOKEN_NOT_FOUND);
18-20: 불필요한 Lombok 제거 제안필드가 없어
@RequiredArgsConstructor는 효과가 없습니다. 삭제하면 가독성이 좋아집니다.src/main/java/konkuk/chacall/global/common/security/filter/JwtAuthenticationEntryPoint.java (1)
24-24: 매직 스트링 제거
"exception"은 상수로 추출해 중복·오타를 방지하세요(예:private static final String REQUEST_EXCEPTION_ATTR = "exception";).build.gradle (1)
40-41: 잭슨 직접 선언은 중복 가능성 — Boot가 관리하므로 제거 고려
spring-boot-starter-web이 잭슨 및 JSR310 모듈을 관리합니다. 필요 시 커스텀 버전 핀ning 목적이 아니라면 제거를 권장합니다.- implementation 'com.fasterxml.jackson.core:jackson-databind' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'src/main/java/konkuk/chacall/global/common/exception/JsonParsingException.java (1)
12-13: 예외 메시지/원인 전파를 위한 생성자 보강 제안디버깅/로깅 가독성을 위해 message/cause 생성자를 추가하면 좋습니다.
다음 변경을 제안합니다:
public class JsonParsingException extends RuntimeException { private final ErrorCode errorCode = JSON_PARSING_ERROR; - public JsonParsingException() { - } + public JsonParsingException() {} + public JsonParsingException(String message) { super(message); } + public JsonParsingException(String message, Throwable cause) { super(message, cause); } + public JsonParsingException(Throwable cause) { super(cause); } }src/test/resources/application-test.yml (1)
33-35: 테스트에서 고정 포트(8080) 사용 지양로컬/CI 포트 충돌을 피하려면 랜덤 포트 사용이 안전합니다.
다음과 같이 변경 제안:
-server: - port: 8080 +server: + port: 0src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java (1)
71-74: 타입 불일치 메시지 가독성 개선 (클래스명 노출 형식)
getRequiredType()은class java.lang.Long형태로 출력될 수 있습니다. 간단히 simpleName을 사용하면 사용자 메시지가 깔끔해집니다.- return ResponseEntity - .status(API_INVALID_TYPE.getHttpStatus()) - .body(ErrorResponse.of(API_INVALID_TYPE, e.getName() + "는 " + e.getRequiredType() + " 타입이어야 합니다.")); + String required = java.util.Optional.ofNullable(e.getRequiredType()) + .map(Class::getSimpleName) + .orElse("올바른 타입"); + return ResponseEntity + .status(API_INVALID_TYPE.getHttpStatus()) + .body(ErrorResponse.of(API_INVALID_TYPE, e.getName() + "는 " + required + " 타입이어야 합니다."));src/main/java/konkuk/chacall/global/common/security/property/ServerWebProperties.java (1)
18-19: 허용 도메인 정규화 캐시 제안초기화 시 정규화된 Set으로 캐싱하면 비교 비용을 줄이고 오동작을 예방할 수 있습니다(스레드-세이프하게 교체).
src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java (1)
24-27: 미사용 의존성 정리
UserRepository,JwtUtil이 현재 메서드에서 사용되지 않습니다. 주입 제거로 간결화하세요.- private final UserRepository userRepository; - private final JwtUtil jwtUtil; - private final LoginTokenStorage loginTokenStorage;src/main/java/konkuk/chacall/global/common/security/filter/JwtAuthenticationFilter.java (1)
71-79: Bearer 파싱 견고성 개선
split(" ")[1]은 공백 수/형식에 취약합니다. prefix 길이 기반substring+trim이 안전합니다.- String authorization = request.getHeader(JWT_HEADER_KEY.getValue()); - if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue())) { - return authorization.split(" ")[1]; - } + String authorization = request.getHeader(JWT_HEADER_KEY.getValue()); + if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue() + " ")) { + return authorization.substring((JWT_PREFIX.getValue() + " ").length()).trim(); + }src/main/resources/application-local.yml (4)
32-37: spring.web.resources.add-mappings가 문서 간 상충합니다한 문서에서 false, 다른 문서에서 true로 설정되어 혼동을 유발합니다. 로컬 테스트가 목적이라면 마지막 설정만 유지하거나 프로필 분기(activate.on-profile)를 사용해 충돌을 제거하세요.
가능한 정리 예시(diff는 개념적 제안):
-# NoHandlerFoundException을 위한 옵션 -spring: - web: - resources: - add-mappings: falseAlso applies to: 76-80
56-56: redirect-uri 하드코딩 대신 템플릿 사용 권장프록시/도메인 변경에 안전하도록 Spring 권장 템플릿을 사용하세요.
적용 diff:
- redirect-uri: http://localhost:8080/login/oauth2/code/kakao + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
71-74: 액세스 토큰 만료 30일은 장기 설정 — 운영 전 재검토 권장장기 액세스 토큰은 유출 시 리스크가 큽니다. 리프레시 토큰 전략 또는 더 짧은 만료로의 전환을 검토하세요.
80-80: 파일 끝 개행 누락도구 경고(new-line-at-end-of-file). 개행을 추가해주세요.
src/main/java/konkuk/chacall/global/common/security/oauth2/KakaoUserDetails.java (1)
3-3: 불필요한 import 정리LinkedHashMap 미사용으로 보입니다.
-import java.util.LinkedHashMap;src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java (2)
59-63: Principal 캐스팅 안전성 — 타입 가드 추가 권장환경에 따라 다른 Authentication이 들어올 수 있습니다. instanceof 가드로 방어해 주세요.
적용 diff:
- CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); + if (!(authentication.getPrincipal() instanceof CustomOAuth2User oAuth2User)) { + throw new IllegalStateException("Unexpected principal: " + authentication.getPrincipal().getClass().getName()); + }
81-83: 리다이렉트 경로 선행 '/' 확인됨 — UriComponentsBuilder 사용 권장확인: src/main/java/konkuk/chacall/global/common/security/constant/AuthParameters.java의 REDIRECT_ROLE_SELECT_URL/REDIRECT_MEMBER_HOME_URL/REDIRECT_OWNER_HOME_URL 값은 각각 "/index.html", "/member", "/owner"로 선행 '/'가 있습니다.
권장: 향후 파라미터 확장·인코딩·중복 슬래시 방지를 위해 UriComponentsBuilder.fromHttpUrl(baseOrigin).path(path).queryParam("loginTokenKey", loginTokenKey).build().toUriString() 등으로 URL을 구성하세요.src/main/java/konkuk/chacall/global/common/security/resolver/CustomAuthorizationRequestResolver.java (2)
44-51: 세션 사용 시점 최소화 검토isAllowed가 false인 경우 세션을 생성하지 않는 것은 좋습니다. 추가로, 허용 실패/파싱 실패 케이스에 debug 로그를 남겨 장애 분석 용이성을 확보하세요. 위 패치에 포함되어 있습니다.
47-55: redirect_uri 혼동 위험 — origin으로 정규화하고 additionalParameters 복사 제거 권장
- 위치: src/main/java/konkuk/chacall/global/common/security/resolver/CustomAuthorizationRequestResolver.java (customize, ~47–55행) — 현재 request.getParameter("redirect_uri")를 세션에 그대로 저장함. CustomSuccessHandler는 세션값을 baseOrigin으로 사용해 baseOrigin + path로 리다이렉트하므로 전체 URL이 저장되면 잘못된 리다이렉트가 발생함. 입력값을 URI로 파싱해 scheme://authority(origin)만 세션에 저장하도록 변경하세요.
- 파라미터명 충돌 방지: OAuth2 표준의 redirect_uri와 혼동되므로 파라미터명을 "return_to" 또는 "origin"으로 변경하거나, 하위호환을 위해 둘 다 허용하도록 하세요.
- 불필요한 복사 제거: OAuth2AuthorizationRequest.from(base).additionalParameters(new HashMap<>(base.getAdditionalParameters()))는 생략하고 OAuth2AuthorizationRequest.from(base).build()로 단순화 권장.
src/main/java/konkuk/chacall/global/common/security/util/JwtUtil.java (3)
30-37: 만료 설정 누락 시 즉시 실패 처리프로퍼티 미설정/0 이하 값이면 즉시 실패하도록 가드 추가를 권장합니다.
public String createAccessToken(Long userId) { + if (tokenExpiredMs <= 0) { + throw new IllegalStateException("jwt.access-expiration must be > 0"); + } return Jwts.builder()
57-63: 반환 타입 원시 boolean 권장 + 파서 재사용- public Boolean isExpired(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); - } + public boolean isExpired(String token) { + return jwtParser.parseSignedClaims(token) + .getPayload() + .getExpiration() + .before(new Date()); + } @@ - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(JWT_ACCESS_TOKEN_KEY.getValue(), Long.class); + return jwtParser.parseSignedClaims(token) + .getPayload() + .get(JWT_ACCESS_TOKEN_KEY.getValue(), Long.class);
30-37: subject/issuer 설정 고려(선택)추후 감사/추적을 위해 subject(=userId 문자열) 또는 issuer를 설정해 두면 분석이 용이합니다. 필수는 아닙니다.
src/main/java/konkuk/chacall/global/config/SecurityConfig.java (4)
57-63: 주석과 실제 동작 불일치주석에는 “additionalParameters.return_to 저장”이라고 되어 있으나, 실제로는 세션에만 저장합니다. 혼동 방지를 위해 주석을 정정하세요.
- // redirect_uri(origin) → additionalParameters.return_to 저장 + // redirect_uri(origin) → (Resolver가) 세션에 origin 저장
64-66: STATELESS + OAuth2 세션 공존 설명 보강SessionCreationPolicy.STATELESS와 HttpSessionOAuth2AuthorizationRequestRepository의 제한적 세션 사용은 공존 가능합니다. 유지보수성을 위해 “OAuth2 인가 요청 단계에서만 세션 생성”임을 주석으로 명확히 해두는 것을 권장합니다.
96-106: CORS 도메인 패턴/널 가드
- 서브도메인(예: *.example.com) 지원 필요 시 setAllowedOriginPatterns 사용을 고려하세요.
- getWebDomainUrls()가 빈/널일 때 NPE를 피하도록 기본값 처리해 두세요.
45-52: 테스트 전용 엔드포인트는 프로파일로 가드하세요 — SecurityConfig.java (라인 45–52)
/index.html 및 /static/** 허용은 테스트용으로 보이므로 프로덕션 노출을 막고 application-{profile}.properties 기반 화이트리스트 병합 또는 ActiveProfiles/ConditionalOnProperty 검사로 local/dev에서만 허용하도록 변경하세요.src/main/java/konkuk/chacall/domain/user/domain/User.java (3)
42-50: 정규화/검증 보강 제안(선택)createNewUser 인자에 대해 trim/길이 검증 등을 적용하면 유효성 일관성이 좋아집니다. (예: name 최대 20자 초과시 예외)
22-40: DDL 변경에 대한 마이그레이션 안내role length 상향, email/profileImageUrl 제약 변경 시 DDL 마이그레이션이 필요합니다. 운영 DB에 인덱스/제약 재생성 시간 및 롤백 플랜을 준비하세요.
25-26: 프로필 이미지 URL 컬럼 제약 완화 권장 (User.profileImageUrl)현재 @column(nullable = false)로 설정되어 있어 JPA 기본 VARCHAR(255) 제한에 걸릴 수 있습니다. 외부 프로필 이미지 URL은 255자 초과가 흔하므로 length=500 또는 TEXT/CLOB로 확장하고 nullable 허용을 권장합니다.
파일: src/main/java/konkuk/chacall/domain/user/domain/User.java
- @Column(nullable = false) + @Column(length = 500, nullable = true) private String profileImageUrl;
- src/main/java/konkuk/chacall/global/common/security/oauth2/KakaoUserDetails.java:40 에서 profile.get("profile_image_url").toString() 호출 — 값이 없을 경우 NPE 발생 가능하므로 null-safe 처리 필요.
- 운영 DB가 이미 존재하면 컬럼 변경용 마이그레이션 추가 필요(현재 repo에 src/main/resources/db/migration 없음).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (42)
build.gradle(1 hunks)src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java(1 hunks)src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java(1 hunks)src/main/java/konkuk/chacall/domain/foodtruck/domain/Rating.java(1 hunks)src/main/java/konkuk/chacall/domain/user/domain/User.java(2 hunks)src/main/java/konkuk/chacall/domain/user/domain/repository/UserRepository.java(1 hunks)src/main/java/konkuk/chacall/domain/user/domain/value/Gender.java(1 hunks)src/main/java/konkuk/chacall/domain/user/domain/value/Role.java(1 hunks)src/main/java/konkuk/chacall/domain/user/member/domain/SavedFoodTruck.java(1 hunks)src/main/java/konkuk/chacall/domain/user/owner/domain/BankAccount.java(1 hunks)src/main/java/konkuk/chacall/domain/user/owner/domain/ChatTemplate.java(1 hunks)src/main/java/konkuk/chacall/global/common/exception/JsonParsingException.java(1 hunks)src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java(2 hunks)src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java(2 hunks)src/main/java/konkuk/chacall/global/common/security/annotation/UserId.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/constant/AuthParameters.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/filter/JwtAuthenticationEntryPoint.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/filter/JwtAuthenticationFilter.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/KakaoUserDetails.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/LoginUser.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/OAuth2UserDetails.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/auth/dto/AuthTokenRequest.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/auth/dto/AuthTokenResponse.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/LoginTokenStorage.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/property/ServerWebProperties.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/resolver/CustomAuthorizationRequestResolver.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/resolver/UserIdArgumentResolver.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/util/JwtUtil.java(1 hunks)src/main/java/konkuk/chacall/global/config/JsonConfig.java(1 hunks)src/main/java/konkuk/chacall/global/config/RedisConfig.java(1 hunks)src/main/java/konkuk/chacall/global/config/SecurityConfig.java(1 hunks)src/main/resources/application-dev.yml(1 hunks)src/main/resources/application-local.yml(1 hunks)src/main/resources/static/index.html(1 hunks)src/test/java/konkuk/chacall/ChacallApplicationTests.java(1 hunks)src/test/resources/application-test.yml(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/konkuk/chacall/global/common/security/util/JwtUtil.java (1)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java (1)
Slf4j(26-84)
src/main/java/konkuk/chacall/global/common/security/resolver/CustomAuthorizationRequestResolver.java (1)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java (1)
Slf4j(26-84)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java (2)
src/main/java/konkuk/chacall/global/common/security/resolver/CustomAuthorizationRequestResolver.java (1)
Slf4j(18-57)src/main/java/konkuk/chacall/global/common/security/util/JwtUtil.java (1)
Slf4j(17-71)
🪛 Gitleaks (8.27.2)
src/test/resources/application-test.yml
[high] 49-49: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🪛 YAMLlint (1.37.1)
src/test/resources/application-test.yml
[error] 67-67: no new line character at the end of file
(new-line-at-end-of-file)
src/main/resources/application-local.yml
[error] 80-80: no new line character at the end of file
(new-line-at-end-of-file)
src/main/resources/application-dev.yml
[error] 79-79: no new line character at the end of file
(new-line-at-end-of-file)
🔇 Additional comments (34)
src/main/resources/application-dev.yml (3)
79-79: 파일 끝 개행 누락.일부 도구에서 경고가 발생합니다. EOF 개행을 추가하세요.
[raise_nitpick_refactor]
39-44: dev 프로파일 허용 도메인 정리 필요 — 코드 검색 실패로 사용처 미확인src/main/resources/application-dev.yml(라인 39–44)에 prod 도메인 https://chacall.co.kr 가 포함되어 있습니다. dev에는 로컬/사내 도메인만 두고 prod 프로파일에서 prod 도메인을 허용하도록 분리하세요. 소비부(ServerWebProperties 등)에서 스킴/포트/슬래시 정규화 여부도 확인하세요.
검증 시도: 제공한 rg 명령이 "No files were searched"를 반환해 바인딩·사용처를 확인하지 못했습니다. 레포 루트에서 아래 명령을 실행해 사용처를 찾아 결과를 붙여 주세요.
#!/bin/bash # 레포 루트에서 실행 rg -nC2 -uu --hidden -g '!**/build/**' -P 'web-domain-urls|ServerWebProperties|@ConfigurationProperties' || true
69-73: JWT 접근 토큰(30일) — prod 분리 및 JWT_SECRET 강도·롤테이션 필요dev에서 30일 접근 토큰은 편리하지만 운영 환경에서는 Access 토큰 수명 단축(권장: 15m–1h) 및 Refresh 토큰 별도 발급을 권장합니다. JWT_SECRET은 HS256 기준 최소 256비트(32바이트) 랜덤 키 이상 사용하고 주기적 롤테이션 정책을 마련하세요.
- 위치: src/main/resources/application-dev.yml (Lines 69–73) — 현재: jwt.secret: ${JWT_SECRET}, access-expiration: 2592000000 (30일).
- 확인 항목: production 프로파일(application-prod.yml / 환경변수 / 시크릿 저장소)에서 (1) Access 토큰 수명 단축 적용 여부, (2) Refresh 토큰 분리 발급·수명 설정 여부, (3) JWT_SECRET 길이(>=32바이트)·공급원·롤테이션 정책을 검증.
- 레포 검색(대체 명령 예시):
grep -RIn --exclude-dir={build,node_modules,target} --include='.yml' -e '^\sjwt:' .
grep -RIn --exclude-dir={build,node_modules,target} -e 'JWT_SECRET' .src/main/java/konkuk/chacall/global/common/security/oauth2/auth/dto/AuthTokenResponse.java (1)
6-9: LGTM – 단순 DTO에 정적 팩토리 적절현재 용도에 충분합니다.
src/main/java/konkuk/chacall/global/common/security/constant/AuthParameters.java (2)
8-8: "Bearer " 접두사 공백 포함 사용 시 파싱 일관성 확인 필요
JWT_PREFIX("Bearer ")끝 공백이 포함되어 있습니다. 토큰 파싱부에서startsWith(JWT_PREFIX.getValue())/substring(...)기준이 동일한지 점검해 주세요. 서로 다른 하드코딩 문자열 사용 시 인증 실패가 발생합니다.
9-10: redirect_uri 오픈 리다이렉트 방어 적용 여부 확인 및 적용 필요src/main/java/konkuk/chacall/global/common/security/constant/AuthParameters.java에 REDIRECT_URI_KEY가 정의되어 있으나 저장소에서 CustomAuthorizationRequestResolver 또는 redirect_uri를 검증·정규화하는 로직을 찾을 수 없습니다. 리다이렉트 파라미터에 대해 허용 도메인 화이트리스트, 스킴/호스트/포트 검사 또는 상대경로만 허용 등 엄격한 검증과 canonicalization(경로 정규화·디코딩 포함)을 적용했는지 확인하고, 미구현이면 즉시 추가하세요.
src/main/java/konkuk/chacall/domain/user/owner/domain/BankAccount.java (1)
4-4: User 경로 변경 반영 LGTM패키지 경로만 갱신되었고 매핑에는 영향이 없습니다.
src/main/java/konkuk/chacall/domain/user/member/domain/SavedFoodTruck.java (1)
5-5: User 경로 변경 반영 LGTM매핑 변경 없음 확인.
src/main/java/konkuk/chacall/domain/foodtruck/domain/Rating.java (1)
4-4: User 경로 변경 반영 LGTM도메인 매핑에는 영향 없음.
src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java (1)
4-4: User PK 컬럼(user_id) 참조 일치 — 확인됨User 엔티티에 @column(name = "user_id")가 선언되어 있고 ChatRoom의 referencedColumnName = "user_id"과 일치합니다. 확인 위치: src/main/java/konkuk/chacall/domain/user/domain/User.java (userId), src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java (JoinColumn)
src/main/java/konkuk/chacall/global/common/security/resolver/UserIdArgumentResolver.java (1)
34-36: 필터에서 JWT_ACCESS_TOKEN_KEY로 request.setAttribute 설정 및 타입 확인 필요자동 스크립트가 저장소 파일을 찾지 못해 수동 확인이 필요합니다.
- 위치: src/main/java/konkuk/chacall/global/common/security/resolver/UserIdArgumentResolver.java (약 34–36행)
- 확인 항목:
- 필터/인터셉터 등에서 request.setAttribute(JWT_ACCESS_TOKEN_KEY.getValue(), …) 호출이 실제로 존재하는지
- 저장되는 값의 실제 타입(예: Long/Integer/String/사용자 객체 등)
- 값이 null일 때의 처리(현재 AuthException(AUTH_TOKEN_NOT_FOUND) 발생) — 필요 시 타입 체크/널 처리 보강
src/test/java/konkuk/chacall/ChacallApplicationTests.java (2)
8-8: 테스트 프로파일 활성화 LGTM@activeprofiles("test") 적용으로 테스트 설정이 제대로 로드됩니다.
5-6: 코멘트 없음변경된 import에 대해 별도 피드백 없습니다.
src/main/java/konkuk/chacall/domain/user/domain/value/Gender.java (1)
1-1: 패키지 이동 정합성 OK도메인 계층 구조(domain.user.domain.value)로의 이동이 일관됩니다.
src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java (1)
5-5: User import 경로 업데이트 LGTM리팩터된 User 패키지 경로와 일치합니다.
src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
5-5: User import 경로 업데이트 LGTM연관관계(owner) 타입이 새 패키지 경로와 정합적입니다.
src/main/java/konkuk/chacall/domain/user/owner/domain/ChatTemplate.java (1)
4-4: User import 경로 업데이트 LGTM소유자 연관관계 타입이 리팩터 후 경로와 일치합니다.
src/test/resources/application-test.yml (2)
22-26: Redis 테스트 설정 LGTM로컬 Redis(host: localhost, port: 6379)를 명시한 테스트 환경 구성은 합리적입니다.
CI 환경에 Redis가 없을 경우를 대비해 test profile에서 임베디드 Redis 또는 Testcontainers 사용을 검토하시겠습니까?
47-56: OAuth2 클라이언트 ID/시크릿 하드코딩 제거 필요테스트 프로파일이라도 크리덴셜은 환경변수로 주입해 유출 리스크와 gitleaks 알람을 줄이세요.
파일: src/test/resources/application-test.yml (약 47–56행)
registration: kakao: client-name: kakao - client-id: abcd1234efgh5678ijkl90mnopqrstu - client-secret: abcdefghij1234567890klmnopqrstuv + client-id: ${KAKAO_CLIENT_ID:test-client-id} + client-secret: ${KAKAO_CLIENT_SECRET:test-client-secret} redirect-uri: http://localhost:8080/login/oauth2/code/kakao authorization-grant-type: authorization_code client-authentication-method: client_secret_post scope: - profile_nickname - profile_image - account_email제공하신 rg 실행 결과가 "No files were searched"를 반환해 전체 검사에 실패했습니다. 전역 하드코딩된 비밀정보 존재 여부 확인을 위해 아래 수정된 명령을 실행하고 결과를 붙여 올려주세요:
#!/bin/bash rg -nP -S '(client-secret|client_secret|client_id|jwt:\s*secret|accessKey|secretKey|access_key|secret_key|api_key)' --hiddensrc/main/java/konkuk/chacall/domain/user/domain/value/Role.java (3)
7-9: NON_SELECTED 권한 플로우 검증 필요Authentication/Authorization에서
NON_SELECTED가 부여될 때의 권한 맵핑(GrantedAuthority)과 리다이렉트 분기(뷰 선택 전용 등) 로직이 모두 반영되었는지 확인 부탁드립니다.
5-17: 패키지 이동 및 enum 확장 LGTM의미적 네이밍/패키징 정돈과 한글 라벨 유지 모두 적절합니다.
6-10: JPA enum 매핑 확인 필요 — EnumType.ORDINAL 사용 시 데이터/권한 붕괴 위험Role에 NON_SELECTED가 추가되어 EnumType.ORDINAL로 저장 중이면 기존 ordinal이 밀려 권한/데이터 불일치가 발생합니다. 엔티티가 @Enumerated(EnumType.STRING)로 매핑되어 있는지 확인하세요.
레포지토리 루트에서 다음 명령을 실행하고 출력 결과를 붙여 주세요:
rg -n -uu -S '@Enumerated\(\s*EnumType\.ORDINAL\s*\)' || true rg -n -uu -S 'EnumType\.ORDINAL' || true rg -n -uu -S '@Enumerated\(' || true rg -n -uu -S '\bRole\b' || true git grep -n 'EnumType.ORDINAL' || true grep -RIn --exclude-dir={.git,node_modules,target} 'EnumType.ORDINAL' . || truesrc/main/java/konkuk/chacall/global/common/security/oauth2/LoginUser.java (1)
5-12: 레코드/팩토리 구성은 간결하고 목적에 부합합니다OAuth2/토큰 재구성 경로 분리 목적에 맞습니다.
src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java (1)
168-176: JSON 파싱 예외 핸들러 추가 LGTM에러코드-상태 매핑과 로그 레벨 사용이 적절합니다.
src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java (1)
27-41: GETDEL 의존성(원자 소비) 환경 확인 및 예외 메시지 보강 제안
getAndDelete는 Redis 6.2+의 GETDEL을 사용합니다. 환경이 6.2 미만이면 원자성이 보장되지 않을 수 있습니다. 또한 파싱 실패 시 원인 기록이 없어 운영 분석이 어렵습니다.다음 항목 확인 부탁드립니다.
- 프로덕션 Redis 버전이 6.2+인지
- 직렬화 설정(JDK/JSON) 및 호환성
필요 시, 문자열 인코딩 방식(옵션 A)을 적용하고, 파싱 실패 시 상세 사유를 로그/에러에 포함하세요.
src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java (1)
43-49: 토큰 소비 후 응답 설계 확인현재는 Redis의 토큰 값을 그대로 반환합니다. 스펙상 “발급”이 맞다면 여기서 새 Access Token을 생성해 반환하는지가 맞는지 검토해 주세요(성공 핸들러에서 저장된 값의 의미 정의 필요).
src/main/java/konkuk/chacall/global/common/security/filter/JwtAuthenticationFilter.java (3)
86-95: 화이트리스트 재확인 (/auth/users, /index.html 제외 시점)
/auth/users경로는 본 PR에서 보이지 않습니다./index.html은 테스트용으로 주석/프로필 조건부가 안전합니다. 미사용/불필요 경로는 제거해 공격 표면을 줄이세요.
49-54: 예외 처리 흐름 확인(401 응답 책임 위치)필터에서 예외를 request attribute로만 전달하고 체인을 계속합니다.
AuthenticationEntryPoint또는 전역 예외 처리에서 401/403을 일관되게 반환하는지 SecurityConfig 흐름을 확인해 주세요.
40-48: 핵심 플로우 LGTMJWT 검증 → LoginUser 복원 → SecurityContext 주입 흐름이 명확합니다.
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java (1)
41-43: 역할 미선택 판별 메서드 적절간결하고 의도가 명확합니다.
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java (1)
49-56: 허용 오리진 폴백 로직 합리적세션 미존재/검증 실패 시 설정값으로 안전하게 폴백하는 흐름 좋습니다.
폴백 도메인의 선택 기준(리스트 0번)이 의도와 일치하는지 운영 설정과 함께 한번만 확인 부탁드립니다.
src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/LoginTokenStorage.java (1)
11-17: 원자성 확인 — getAndDelete 사용됨src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java (라인 30)에서 redisTemplate.opsForValue().getAndDelete(redisKey)를 호출합니다. Spring Data Redis의 getAndDelete는 Redis GETDEL과 대응되며, GETDEL은 Redis 6.2.0 이상에서 제공되는 원자적 명령입니다. 배포 환경의 Redis 버전이 6.2 이상인지 확인하세요. (docs.spring.io)
src/main/java/konkuk/chacall/domain/user/domain/User.java (2)
34-36: 부정확 — Gender enum이 'M'/'F'라 @column(length = 1)은 적절합니다.
Gender는 M, F로 정의되어 있어 EnumType.STRING으로 DB에 'M' 또는 'F'가 저장되므로 @column(length = 1)을 변경할 필요가 없습니다.Likely an incorrect or invalid review comment.
1-51: 패키지 이동 후 잔존 import 없음 — 확인됨rg 스캔 결과 18건이 매치되었으나 모두 새 패키지(konkuk.chacall.domain.user.domain.*) 관련 import/참조이며, 이전 FQCN "konkuk.chacall.domain.user.User"는 발견되지 않았습니다.
| 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.
🛠️ Refactor suggestion
JJWT 모듈 가시성 축소: impl/jackson은 runtimeOnly로 전환 권장
API만 컴파일 타임에 노출하고 구현체는 런타임 의존성으로 두는 게 일반적입니다.
- 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 'io.jsonwebtoken:jjwt-api:0.12.3'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'📝 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 'io.jsonwebtoken:jjwt-api:0.12.3' | |
| implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' | |
| implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' | |
| implementation 'io.jsonwebtoken:jjwt-api:0.12.3' | |
| runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' | |
| runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' |
🤖 Prompt for AI Agents
In build.gradle around lines 47 to 49, the JJWT implementation artifacts are
currently declared as implementation; change only the API artifact
(io.jsonwebtoken:jjwt-api:0.12.3) to remain on the compile classpath and move
the implementation artifacts (io.jsonwebtoken:jjwt-impl:0.12.3 and
io.jsonwebtoken:jjwt-jackson:0.12.3) to runtimeOnly so the API is available at
compile time but concrete implementations are provided only at runtime.
| implementation 'org.springframework.security:spring-security-oauth2-client' | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Security OAuth2는 Boot 스타터 사용 권장
순수 spring-security-oauth2-client 대신 부트 스타터를 사용하면 버전 정합성과 자동 설정이 수월합니다.
- implementation 'org.springframework.security:spring-security-oauth2-client'
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'📝 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.security:spring-security-oauth2-client' | |
| implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' |
🤖 Prompt for AI Agents
In build.gradle around lines 52-53, replace the direct dependency on
org.springframework.security:spring-security-oauth2-client with the Spring Boot
starter to let Boot manage versions and auto-configuration; change the
dependency to org.springframework.boot:spring-boot-starter-oauth2-client (using
implementation), remove any explicit version for the security client, and then
refresh/refresh dependencies (gradle --refresh-dependencies or IDE sync) to
ensure version alignment with your Spring Boot parent.
| @Repository | ||
| public interface UserRepository extends JpaRepository<User, Long> { | ||
| Optional<User> findByKakaoId(String kakaoId); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
kakaoId에 유니크 제약/인덱스 필요
OAuth 식별자 중복은 계정 합치기/권한오류로 직결됩니다. DB/엔티티 수준의 유니크 제약을 보장해 주세요.
유니크 제약 존재 여부를 확인하는 스크립트입니다(엔티티/DDL 모두 탐색):
🏁 Script executed:
#!/bin/bash
# kakaoId 필드에 unique=true 또는 테이블 유니크 제약이 있는지 검색
echo "== 엔티티 어노테이션 확인 =="
rg -nP --type=java -C2 '(?s)@Column\([^)]*unique\s*=\s*true[^)]*\)\s*\R\s*private\s+String\s+kakaoId\b' || echo "엔티티에 unique=true 미발견"
echo "== 테이블 레벨 UniqueConstraint 확인 =="
rg -nP --type=java -C2 '@Table\([^)]*uniqueConstraints' || echo "테이블 레벨 uniqueConstraints 미발견"
echo "== 마이그레이션/DDL에서 인덱스/제약 확인 =="
rg -nP -C2 '(unique\s+index|unique\s+key|unique\s*\(|create\s+unique\s+index).*kakao' -g '**/*.{sql,yml,yaml}' || echo "DDL에서 kakao 관련 유니크 제약/인덱스 미발견"Length of output: 160
긴급: kakaoId에 엔티티·DB 수준 유니크 제약/인덱스 추가 필요
검증 스크립트 결과 엔티티(@Column/@table)와 마이그레이션/DDL에서 kakaoId 관련 유니크 제약/인덱스가 발견되지 않았습니다. 중복 OAuth 식별자는 계정 합치기·권한 오류로 직결됩니다.
- 엔티티: src/main/java/konkuk/chacall/domain/user/domain/User.java의 kakaoId 필드에 @column(unique = true) 또는 클래스 레벨 @table(uniqueConstraints = ...) 추가
- DB(마이그레이션): 실제 테이블에 대해 고유 제약/고유 인덱스 추가(예: CREATE UNIQUE INDEX ON (kakao_id) 또는 ALTER TABLE … ADD CONSTRAINT UNIQUE (kakao_id))
🤖 Prompt for AI Agents
In src/main/java/konkuk/chacall/domain/user/domain/repository/UserRepository.java around lines 9 to 12: the review found there is no entity- or DB-level unique constraint/index on kakaoId which can allow duplicate OAuth identifiers; update the User entity (src/main/java/konkuk/chacall/domain/user/domain/User.java) to mark the kakaoId column unique (either @Column(unique = true) on the field or add @Table(uniqueConstraints = @UniqueConstraint(columnNames = "kakao_id")) at the class level) and add a database migration that creates a unique constraint/index on the physical column (e.g., ALTER TABLE user ADD CONSTRAINT uq_user_kakao_id UNIQUE (kakao_id) or CREATE UNIQUE INDEX ux_user_kakao_id ON user(kakao_id)), ensuring column name matches the actual DB column and handling existing duplicates before applying the migration.
| Exception e = (Exception) request.getAttribute("exception"); | ||
| if(e == null){ | ||
| e = authException; | ||
| } | ||
| resolver.resolveException(request, response, null, e); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
예외 위임 실패 시 401로 안전하게 폴백하세요.
리졸버가 응답을 쓰지 않으면 성공(200)으로 남을 수 있습니다. 커밋 여부 확인 후 401로 폴백하세요.
- Exception e = (Exception) request.getAttribute("exception");
+ Exception e = (Exception) request.getAttribute("exception");
if(e == null){
e = authException;
}
- resolver.resolveException(request, response, null, e);
+ resolver.resolveException(request, response, null, e);
+ if (!response.isCommitted()) {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ }📝 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.
| Exception e = (Exception) request.getAttribute("exception"); | |
| if(e == null){ | |
| e = authException; | |
| } | |
| resolver.resolveException(request, response, null, e); | |
| } | |
| Exception e = (Exception) request.getAttribute("exception"); | |
| if(e == null){ | |
| e = authException; | |
| } | |
| resolver.resolveException(request, response, null, e); | |
| if (!response.isCommitted()) { | |
| response.sendError(HttpServletResponse.SC_UNAUTHORIZED); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/global/common/security/filter/JwtAuthenticationEntryPoint.java
around lines 24-29, the current logic delegates an exception to the resolver but
doesn't handle the case where the resolver fails to write a response; update the
method to call resolver.resolveException(request, response, null, e) and then
check response.isCommitted(); if the response is not committed, set the HTTP
status to 401 (Unauthorized) and ensure a minimal safe response is written
(status only or a short message) so the client does not receive a 200 OK by
mistake.
ksg1227
left a comment
There was a problem hiding this comment.
이미 어느정도 구현을 해보신 내용이다보니 전체적으로 흠 잡을 부분 없이 좋은 것 같습니다.
다만 보안적인 아쉬움이 어쩔 수 없이 생기긴 하더라구요. 이건 토큰을 사용하지 못하는 현재 상황에서는 어쩔 수 없는 문제이긴 한데, 제가 이해하기로 현재 인증 로직은
- 소셜 로그인 성공 시 토큰 발급을 위한 loginTokenKey 값을 리다이렉트 url 에 쿼리 파라미터로 포함시켜 반환한다.
- 클라이언트는 이 쿼리 파라미터에 포함된 loginTokenKey 값을 추출해서, 토큰 발급 API 에 요청을 보냄으로써 access 토큰을 발급받는다. (사실 로그인 성공시 토큰을 무조건 발급 받아야하지만, 쿠키로 발급이 현재 상황에선 힘들어서 이렇게 한 번에 depth 가 추가된 상황)
으로 이해하고있습니다.
이러한 흐름상에서 쿠키를 사용하지 않고 토큰을 발급하는 방식 중에는,
- 쿼리 파라미터에 access 토큰 자체를 포함시켜서 반환하는 방식
- 현재 방식
이 대표적인 것 같은데, 첫번째 방식은 보안적으로 따져보았을 때 access 토큰이 바로 노출되기 때문에 가장 보안성이 떨어지는 방식이라고 생각됩니다.
두번째 방식은 쿼리 파라미터에 바로 토큰을 포함시키지는 않지만, 사실상 loginTokenKey 값을 확인한 후, 토큰 발급 API 경로만 알아내서 요청을 보낸다면, 누구나 access 토큰을 발급받을 수 있는 구조인 것 같긴 합니다.
그래서 토큰이 바로 보여지냐, 한 번의 과정을 더 거쳐서 토큰을 확인하느냐 이 차이인 것 같아서, 확실히 현재 방식도 보안적인 측면에서는 다소 아쉬움이 생기네요
우선 현재로서는 해결이 어려우니까 다음에 같이 고민하고 해결 ㄱㄱ 해보시죠
고생띠하셨습니다
| .orElseGet(() -> { | ||
| User newUser = userRepository.save(User.createNewUser(nickname, profileImage, kakaoId, email)); | ||
| return new CustomOAuth2User(LoginUser.createLoginUser(kakaoId, newUser.getUserId(), newUser.getRole())); |
There was a problem hiding this comment.
이 부분에서 기존 사용자가 존재하지 않는 경우, 신규 사용자를 등록하는 로직이 존재하기 때문에, @transactional 을 메서드에 붙여주는 것도 좋을 것 같아요~
|
|
||
| String redirectUri = request.getParameter(REDIRECT_URI_KEY.getValue()); | ||
| Map<String, Object> additional = new HashMap<>(base.getAdditionalParameters()); | ||
| if (StringUtils.hasText(redirectUri) && serverWebProperties.isAllowed(redirectUri)) { |
There was a problem hiding this comment.
사전에 허용된 주소인지 검증하는 과정 거치기 좋네요 굿
| request.getSession(true).setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUri); | ||
| } |
There was a problem hiding this comment.
처음에는 그냥 request.setAttribute 만 하면 되는 것 아닌가? 굳이 세션에 저장해야하는가? 의문점이 들어서 찾아보니, 중간에 리다이렉션을 거치다보니 세션에 저장을 하지 않으면 setAttribute 로 설정한 데이터는 날라간다고하네요, 잘 적용하신 것 같습니다 👍🏻
| if (!serverWebProperties.isAllowed(Objects.toString(baseOrigin, ""))) { | ||
| List<String> origins = serverWebProperties.getWebDomainUrls(); | ||
| if (origins == null || origins.isEmpty()) { | ||
| throw new AuthException(ErrorCode.AUTH_WEB_ORIGIN_EMPTY); | ||
| } | ||
| baseOrigin = origins.get(0); | ||
| } |
There was a problem hiding this comment.
이 부분은 사실상 CustomAuthorizationRequestResolver 쪽의 로직과 비교해보았을 때, 중복된 로직이라고도 볼 수 있을 것 같긴 한데,
Resolver 를 통한 도메인 1차 검증, SuccessHandler 측에서의 2차 검증을 통한 안정성 강화 측면에서 작성된 로직이라고 생각하면 될까요?
There was a problem hiding this comment.
넵 첫번째 Resolver에서 검증은 세션을 추가하냐 안하냐를 판단하기 위한 분기이고 두번째 SuccessHandler에서의 검증은 올바른 redirect_uri가 아니거나 redirect_uri 자체가 비었을 경우를 검증해서 저희가 정한 특정 도메인으로 redirect 되도록 하기 위한 분기입니다!
| if (loginTokenKey == null || loginTokenKey.isBlank()) { | ||
| throw new AuthException(API_INVALID_PARAM, | ||
| new IllegalArgumentException("loginTokenKey는 필수 파라미터입니다.")); | ||
| } |
There was a problem hiding this comment.
이 부분은 Bean Validation을 활용해서 DTO 에 @notblank 를 붙여주는 것이 더 깔끔할 것 같습니당
| public interface LoginTokenStorage { | ||
|
|
||
| void put(String key, Role role, String token, Duration ttl); | ||
|
|
||
| /** | ||
| * 저장된 토큰을 1회용으로 소비 후 삭제한다. | ||
| * 존재하지 않으면 null 반환. | ||
| */ | ||
| Entry consume(String key); | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| final class Entry { | ||
| private Role role; | ||
| private String token; | ||
|
|
||
| public Entry(Role role, String token) { | ||
| this.role = role; | ||
| this.token = token; | ||
| } | ||
| } |
There was a problem hiding this comment.
이 부분을 인터페이스로 관리하시는 이유가 있나요? Redis 를 활용하지 않고, 기존에 THIP 에서 했던 것처럼 로컬에 ConcurrentHashMap 을 만들어서 사용하는 상황, 혹은 그 외의 다른 방식으로 구현 방식을 교체하는 상황을 대비한 것인가요?
=> Entry 클래스에 대해서는 코드래빗 리뷰도 확인해볼만 할 것 같습니다. Record 를 활용해도 좋을 것 같아요
There was a problem hiding this comment.
네넵 처음에 Redis가 잘 동작하지 않을 것에 대비해서 LoginTokenStorage를 인터페이스로 추출하고 MemoryLoginTokenStorage와 RedisLoginTokenStorage를 나눠서 개발하였는데 Redis 방식에 훨씬 괜찮은 것 같아 하나의 방식으로만 구현하도록 결정했습니다. 인터페이스를 제거하는게 나을까요??
There was a problem hiding this comment.
음 근데 어찌되었든 확장에 대한 이정도 대비는 나쁘지 않다고 생각하고, 개인적으로 Entry 클래스를 인터페이스 내부에 만들어서 사용하는 것이 좋은 것 같아서 유지해도 좋을 것 같습니다!
네넵 맞아요. 우선 첫번째 방식은 쿼리 파라미터에 바로 access token이 노출되기 때문에 보안에 매우 취약하다고 알고 있습니다. 현재 구현방식 또한 상균님이 말씀하시는 것처럼 loginTokenKey가 탈취될 경우 보안적으로 문제가 생기긴 할 것 같네요. 다만, 탈취당하더라도 두가지 때문에 조금이나마 1번 방식보다는 낫다고 생각했습니다.
그래서 완벽하진 않지만, 현재 상황에서는 현실적인 차선책이라고 판단했습니다. 나중에 쿠키 기반이나 더 안전한 방식으로 개선할 수 있으면 같이 논의해보면 좋을 것 같습니다! |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java (2)
21-23: NPE 예방: 빈 Map 반환 적용 👍
getAttributes()가 빈 Map을 반환하도록 수정되어 스프링 시큐리티 연동 시 NPE 리스크가 해소되었습니다.
31-39: 권한 컨벤션(ROLE_*) 불일치 및 불필요한 익명 클래스 — SimpleGrantedAuthority로 교체 요망현재 "NORMAL_USER" 문자열은
hasRole/hasAuthority규칙과 어긋나고, 익명 클래스/가변 리스트가 불필요합니다. 로그인 사용자의 실제Role을 반영해"ROLE_" + role.name()형태로 부여하세요.적용 diff (메서드 본문 교체):
@@ @Override public Collection<? extends GrantedAuthority> getAuthorities() { - Collection<GrantedAuthority> authorities = new ArrayList<>(); - authorities.add(new GrantedAuthority() { - @Override - public String getAuthority() { - return "NORMAL_USER"; // 임시 사용자 권한 - } - }); - return authorities; + return Collections.singletonList( + new SimpleGrantedAuthority("ROLE_" + loginUser.role().name()) + ); }추가/정리 import:
@@ -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Map; +import org.springframework.security.core.authority.SimpleGrantedAuthority;src/main/resources/application-dev.yml (2)
39-44: 서버 포트(8000) vs 허용 도메인(8080/5173) 불일치 재확인 필요dev 포트 8000, web-domain-urls에 8080/5173 혼재. CORS·OAuth 리다이렉트 허용목록·Docker EXPOSE와의 일관성 재검증 바랍니다. 이전 커밋에서도 동일 지적이 있었습니다.
다음 스크립트로 레포 내 포트/리다이렉트 값을 한눈에 점검하세요.
#!/bin/bash set -euo pipefail echo "[server.port in yml]"; rg -n -g '!**/build/**' -S '\bserver\.port\s*:\s*\d+' src/**/application*.yml || true echo; echo "[origins/redirect uri hints]"; rg -n -S -g '!**/build/**' 'web-domain-urls|redirect-?uri|localhost:(8000|8080|5173)' || true echo; echo "[Dockerfile EXPOSE]"; rg -n -S 'EXPOSE\s+\d+' Dockerfile docker/** 2>/dev/null || true
45-68: Kakao redirect-uri·scope 점검 및 이메일 미동의 시 처리 보장
- ${KAKAO_REDIRECT_URI}가 콘솔 등록값과 문자 단위로 일치해야 합니다. 배포별 값 검증/부트 시 fail-fast 권장.
- scope에 account_email 포함되어 추가 동의 미수락 시 null 가능. 관련 코드(KakaoUserDetails 등) NPE 위험은 이전에도 지적됨 → null-safe 처리와 폴백 정책이 이 설정과 함께 동작하는지 확인 바랍니다.
- client_authentication_method=client_secret_post는 현재 호환되나 제공자 정책 변동 모니터링 필요.
원하시면 redirect-uri를 표준 패턴({baseUrl}/login/oauth2/code/{registrationId})로 전환하는 옵션 비교안을 드리겠습니다.
src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java (1)
36-36: Bean Validation 활성화(LGTM).
@Valid적용으로 DTO 제약이 정상 동작합니다. 이전 피드백을 반영하셨습니다.
🧹 Nitpick comments (6)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java (2)
14-16: 클래스 불변성 강화(선택): final 권장상속 확장 가능성이 없다면
final class로 선언해 보안/예측 가능성을 높일 수 있습니다.-public class CustomOAuth2User implements OAuth2User { +public final class CustomOAuth2User implements OAuth2User {
42-44: 불리언 메서드 네이밍 컨벤션(선택)
roleNonSelected()→isRoleNonSelected()로 변경하면 불리언 컨벤션에 더 부합합니다.src/main/resources/application-dev.yml (1)
72-72: 파일 끝 개행(Newline at EOF) 추가YAMLlint 경고 해소를 위해 마지막 줄 끝에 개행을 추가하세요.
src/main/java/konkuk/chacall/global/common/security/oauth2/auth/dto/AuthTokenRequest.java (1)
5-9: 입력 값 제약 강화를 권장합니다 (@SiZe 등).공격 표면 축소를 위해 길이 상한을 두는 편이 안전합니다. UUID/랜덤 키 길이를 고려해 보수적으로 8~256자 정도로 제한해 두세요.
적용 예시:
import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; public record AuthTokenRequest( @NotBlank(message = "loginTokenKey는 필수 파라미터입니다.") + @Size(min = 8, max = 256, message = "loginTokenKey 길이가 유효하지 않습니다.") String loginTokenKey ) { }src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java (2)
17-18: 미사용 static import 제거.
API_INVALID_PARAM는 사용되지 않습니다. 정리해 주세요.적용 예시:
-import static konkuk.chacall.global.common.exception.code.ErrorCode.API_INVALID_PARAM; import static konkuk.chacall.global.common.exception.code.ErrorCode.AUTH_INVALID_LOGIN_TOKEN_KEY;
25-27: 미사용 의존성 필드 제거 고려.
UserRepository,JwtUtil이 본 컨트롤러에서 사용되지 않습니다. 추후 사용 계획이 없다면 필드를 제거해 DI 오버헤드와 잡음을 줄이세요.적용 예시:
- private final UserRepository userRepository; - private final JwtUtil jwtUtil;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/auth/dto/AuthTokenRequest.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/LoginTokenStorage.java(1 hunks)src/main/resources/application-dev.yml(1 hunks)src/main/resources/application-local.yml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java
- src/main/resources/application-local.yml
- src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/LoginTokenStorage.java
🧰 Additional context used
🪛 YAMLlint (1.37.1)
src/main/resources/application-dev.yml
[error] 72-72: no new line character at the end of file
(new-line-at-end-of-file)
🔇 Additional comments (4)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java (1)
26-28: getName의 null 가능성 점검
loginUser.kakaoId()가 null일 가능성이 완전히 배제되는지 확인 부탁드립니다. 계약상 non-null이라면 주석/어노테이션으로 명시하거나, 필요 시Objects.requireNonNull로 방어적 코드 추가를 고려하세요.src/main/java/konkuk/chacall/global/common/security/oauth2/auth/dto/AuthTokenRequest.java (1)
5-9: DTO를 record로 정의한 선택과 @notblank 적용은 적절합니다.src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java (2)
40-43: 확인: AUTH_INVALID_LOGIN_TOKEN_KEY는 HTTP 401(UNAUTHORIZED)로 매핑됩니다.ErrorCode enum에 AUTH_INVALID_LOGIN_TOKEN_KEY(HttpStatus.UNAUTHORIZED, 40106, ...)로 선언되어 있고, GlobalExceptionHandler의 AuthException 핸들러가 e.getErrorCode().getHttpStatus()로 상태코드를 설정합니다.
위치: src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java, src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java
34-47: 민감값(토큰) 노출 금지 및 /auth/token 레이트 리밋 권고
- 확인: src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java에서 loginTokenKey를 소비하고 entry.token()을 응답으로 반환함; 해당 파일 내에는 로깅 호출이 보이지 않음.
- 권고: loginTokenKey/토큰 값을 서버 로그(예외·액세스 로그 포함)에 남기지 말 것 — 필요 시 마스킹 또는 해시 사용.
- 권고: /auth/token 엔드포인트에 키·클라이언트·IP별 레이트 리밋 도입(예: Redis INCR + TTL 또는 API 게이트웨이).
- 상태: 전체 저장소 검색이 일부 필터로 스킵되어 다른 파일의 로깅 여부는 확인되지 않음 — 저장소 전체에서 민감값 로깅 유무 재검증 필요.
| @PostMapping("/token") | ||
| public BaseResponse<AuthTokenResponse> getToken( | ||
| @RequestBody @Valid AuthTokenRequest request | ||
| ) { | ||
| String loginTokenKey = request.loginTokenKey(); | ||
|
|
||
| LoginTokenStorage.Entry entry = loginTokenStorage.consume(loginTokenKey); | ||
| if (entry == null) { | ||
| throw new AuthException(AUTH_INVALID_LOGIN_TOKEN_KEY); | ||
| } | ||
|
|
||
| String token = entry.token(); | ||
| return BaseResponse.ok(AuthTokenResponse.of(token)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
loginTokenKey 소비 시 Origin 바인딩 검증을 고려하세요.
CORS만으로는 탈취 시도에 충분치 않습니다. 키 발급 시 저장한 redirect(origin)과 소비 시 Origin/Referer를 대조해 동일 출처에서만 교환되도록 바인딩하면 보안 수준이 크게 올라갑니다(키 탈취 시 재사용 차단).
간단한 방향:
- 키 발급 시 예상 origin 저장(LoginTokenStorage.Entry에 origin 추가).
- 소비 시
request.getHeader("Origin")과 비교 불일치 시 거부.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java
around lines 34-47, add origin-binding checks when consuming the loginTokenKey:
ensure LoginTokenStorage.Entry includes the expected origin at issuance, then
read request.getHeader("Origin") (falling back to "Referer" if Origin is
absent), normalize and compare against entry.getOrigin(), and throw an
AuthException (e.g., AUTH_INVALID_LOGIN_TOKEN_ORIGIN) if they do not match or if
headers are missing; handle nulls safely and keep the existing consume flow
otherwise so only same-origin exchanges succeed.
토큰 응답에 캐시 방지 헤더를 반드시 추가하세요.
액세스 토큰 응답은 브라우저/중간 프록시 캐시에 저장되면 안 됩니다. OAuth2 권고처럼 Cache-Control: no-store, Pragma: no-cache를 설정하세요.
적용 예시:
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
@@
- public BaseResponse<AuthTokenResponse> getToken(
+ public ResponseEntity<BaseResponse<AuthTokenResponse>> getToken(
@RequestBody @Valid AuthTokenRequest request
) {
@@
- String token = entry.token();
- return BaseResponse.ok(AuthTokenResponse.of(token));
+ String token = entry.token();
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(HttpHeaders.CACHE_CONTROL, "no-store");
+ headers.add(HttpHeaders.PRAGMA, "no-cache");
+ return ResponseEntity
+ .ok()
+ .headers(headers)
+ .body(BaseResponse.ok(AuthTokenResponse.of(token)));🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/global/common/security/oauth2/auth/AuthController.java
around lines 34 to 47, the token endpoint currently returns the token body
without cache-prevention headers; modify the method to include HTTP response
headers to prevent caching (e.g., set "Cache-Control: no-store", "Pragma:
no-cache" and optionally "Expires: 0") before returning the token. Implement
this by either returning a ResponseEntity with the headers applied to the
response or by setting the headers on the HttpServletResponse injected into the
controller method, ensuring the BaseResponse<AuthTokenResponse> body remains the
same while the outgoing HTTP headers include the recommended cache-control
values.
| --- | ||
| jwt: | ||
| secret: ${JWT_SECRET} | ||
| access-expiration: 2592000000 # 30일 No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
액세스 토큰 30일 만료는 과도 — 단기 액세스·장기 리프레시로 분리 권장
dev 프로필이라도 길게 습관화되면 프로덕션 유출 위험. access 15분1시간, refresh 730일 구조를 권장합니다.
적용 예시(diff):
jwt:
secret: ${JWT_SECRET}
- access-expiration: 2592000000 # 30일
+ # access 15분, refresh 14일 (필요 시 프로필별 override)
+ access-expiration: 900000 # 15분
+ refresh-expiration: 1209600000 # 14일코드에서 refresh-expiration 사용 여부를 함께 반영해야 합니다.
📝 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.
| --- | |
| jwt: | |
| secret: ${JWT_SECRET} | |
| access-expiration: 2592000000 # 30일 | |
| --- | |
| jwt: | |
| secret: ${JWT_SECRET} | |
| # access 15분, refresh 14일 (필요 시 프로필별 override) | |
| access-expiration: 900000 # 15분 | |
| refresh-expiration: 1209600000 # 14일 |
🧰 Tools
🪛 YAMLlint (1.37.1)
[error] 72-72: no new line character at the end of file
(new-line-at-end-of-file)
🤖 Prompt for AI Agents
In src/main/resources/application-dev.yml around lines 69 to 72, the access
token expiration is set to 30 days which is excessive; change access-expiration
to a short duration (e.g., 15 minutes–1 hour in milliseconds) and add a new
refresh-expiration property (e.g., 7–30 days in milliseconds) in the same jwt
block, and then update the application code/config usage to read and enforce the
new refresh-expiration value where refresh tokens are issued/validated.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java (2)
31-31: PII 로그 노출 위험 — 전체 attributes 로그 제거 필요oAuth2User.getAttributes()에는 이메일/프로필 등 PII가 포함됩니다. info 레벨로 전체를 로그하면 개인정보 유출 리스크가 큽니다. registrationId와 마스킹된 providerId 정도만 debug 레벨로 제한해 주세요.
적용 diff:
- log.info("KakaoUser: {}", oAuth2User.getAttributes()); + // PII 최소화: registrationId 및 마스킹된 providerId만 debug 로깅 + log.debug("OAuth2User loaded. registrationId={}", registrationId); + // Kakao 전용 분기 이후에 providerId 마스킹 로깅 + if (log.isDebugEnabled() && oAuth2UserDetails != null) { + String pid = oAuth2UserDetails.getProviderId(); + String masked = (pid == null) ? "n/a" : pid.substring(0, Math.min(4, pid.length())) + "***"; + log.debug("providerId(4+*)={}", masked); + }Also applies to: 33-37
48-56: find-then-save 경쟁 조건 — UNIQUE(kakao_id) 위반 시 재조회 회복 로직 필요동시 요청에서 둘 다 존재하지 않는다고 판단해 save를 시도하면 UNIQUE(kakao_id) 제약으로 예외가 발생할 수 있습니다. DataIntegrityViolationException 캐치 후 재조회로 회복하는 upsert 패턴을 적용해 주세요.
적용 diff:
+import org.springframework.dao.DataIntegrityViolationException; @@ - CustomOAuth2User customOAuth2User = userRepository.findByKakaoId(kakaoId) + CustomOAuth2User customOAuth2User = userRepository.findByKakaoId(kakaoId) .map(existingUser -> new CustomOAuth2User( LoginUser.createLoginUser(kakaoId, existingUser.getUserId(), existingUser.getRole() ) )) - .orElseGet(() -> { - User newUser = userRepository.save(User.createNewUser(nickname, profileImage, kakaoId, email)); - return new CustomOAuth2User(LoginUser.createLoginUser(kakaoId, newUser.getUserId(), newUser.getRole())); - }); + .orElseGet(() -> { + try { + User newUser = userRepository.save(User.createNewUser(nickname, profileImage, kakaoId, email)); + return new CustomOAuth2User(LoginUser.createLoginUser(kakaoId, newUser.getUserId(), newUser.getRole())); + } catch (DataIntegrityViolationException e) { + return userRepository.findByKakaoId(kakaoId) + .map(u -> new CustomOAuth2User( + LoginUser.createLoginUser(kakaoId, u.getUserId(), u.getRole()) + )) + .orElseThrow(() -> e); + } + });
🧹 Nitpick comments (1)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java (1)
33-41: 제어 흐름 간소화 (가드 절 추천, NPE 회피에 유리)registrationId가 KAKAO가 아니면 즉시 예외로 빠지고, KAKAO일 때만 세부 파싱하도록 가드 절로 정리하면 가독성이 좋아집니다.
적용 diff:
- OAuth2UserDetails oAuth2UserDetails = null; - if (registrationId.equals(KAKAO.getValue())) { - oAuth2UserDetails = new KakaoUserDetails(oAuth2User.getAttributes()); - } - else { + if (!registrationId.equals(KAKAO.getValue())) { log.warn("카카오 로그인만 지원합니다."); throw new AuthException(AUTH_UNSUPPORTED_SOCIAL_LOGIN); - } + } + OAuth2UserDetails oAuth2UserDetails = new KakaoUserDetails(oAuth2User.getAttributes());
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/main/java/konkuk/chacall/domain/user/domain/model/Role.java(1 hunks)src/main/java/konkuk/chacall/domain/user/domain/model/User.java(2 hunks)src/main/java/konkuk/chacall/domain/user/domain/repository/UserRepository.java(1 hunks)src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java(2 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/KakaoUserDetails.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/LoginUser.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/LoginTokenStorage.java(1 hunks)src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
- src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java
- src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java
- src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java
- src/main/java/konkuk/chacall/domain/user/domain/repository/UserRepository.java
- src/main/java/konkuk/chacall/global/common/security/oauth2/tokenstorage/LoginTokenStorage.java
- src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java
- src/main/java/konkuk/chacall/global/common/security/oauth2/LoginUser.java
- src/main/java/konkuk/chacall/global/common/security/oauth2/KakaoUserDetails.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java (1)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomSuccessHandler.java (1)
Slf4j(26-84)
src/main/java/konkuk/chacall/domain/user/domain/model/User.java (8)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2User.java (1)
Getter(14-46)src/main/java/konkuk/chacall/domain/owner/domain/model/BankAccount.java (1)
Entity(10-56)src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java (1)
Entity(12-43)src/main/java/konkuk/chacall/domain/foodtruck/domain/Rating.java (1)
Entity(10-33)src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java (1)
Entity(13-72)src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java (1)
Entity(8-25)src/main/java/konkuk/chacall/domain/owner/domain/model/ChatTemplate.java (1)
Entity(9-24)src/main/java/konkuk/chacall/domain/member/domain/SavedFoodTruck.java (1)
Entity(10-28)
🔇 Additional comments (4)
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java (1)
26-27: @transactional 추가는 타이밍상 적합합니다.신규 생성·조회 트랜잭션 경계를 명확히 해 중간 상태 노출을 줄입니다. 아래 동시성 예외 처리(별도 코멘트)만 보완하면 좋겠습니다.
src/main/java/konkuk/chacall/domain/user/domain/model/User.java (2)
37-38: Role 컬럼 길이 확장(15) LGTMNON_SELECTED(13자) 저장에 충분합니다. 운영 DB 마이그레이션만 누락 없도록 확인해 주세요.
40-48: createNewUser: 외부 입력 Null/길이 방어 필요 — DB 마이그레이션(스키마) 확인 요망
- 문제 요약: createNewUser가 name/profileImageUrl/email을 검증 없이 저장합니다. name은 현재 @column(length = 20)로 카카오 닉네임에 의해 잘림/예외 가능, email/profileImageUrl이 null일 경우 DB 제약과 충돌할 수 있습니다.
- 검증 결과: src/main/resources에서 users 테이블 관련 마이그레이션 파일 없음(마이그레이션/DDL 미확인). OAuth2에서 email/profileImage를 읽는 지점: src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java:44-46 — null/blank 전파 가능.
권장(둘 중 택1):
옵션 A(권장): 엔티티 제약 완화 + 이름 길이 상향- @Column(length = 20, nullable = false) + @Column(length = 50, nullable = false) private String name; - @Column(nullable = false) + @Column(nullable = true) private String profileImageUrl; - @Column(length = 50, nullable = false, unique = true) + @Column(length = 100, nullable = true, unique = true) private String email;옵션 B: 팩토리에서 안전화(컬럼 제약 유지 시; 현재 length=20에 맞춘 예)
- public static User createNewUser(String name, String profileImageUrl, String kakaoId, String email) { - return User.builder() - .name(name) - .profileImageUrl(profileImageUrl) - .kakaoId(kakaoId) - .email(email) - .role(Role.NON_SELECTED) - .build(); - } + public static User createNewUser(String name, String profileImageUrl, String kakaoId, String email) { + String safeName = (name == null || name.isBlank()) ? "사용자" : name.substring(0, Math.min(20, name.length())); + String safeProfile = (profileImageUrl == null) ? "" : profileImageUrl; + String safeEmail = (email == null || email.isBlank()) ? (kakaoId + "@placeholder.kakao") : email; + return User.builder() + .name(safeName) + .profileImageUrl(safeProfile) + .kakaoId(kakaoId) + .email(safeEmail) + .role(Role.NON_SELECTED) + .build(); + }위치: src/main/java/konkuk/chacall/domain/user/domain/model/User.java (createNewUser, ~lines 40–48).
다음 단계: 리포지토리의 실제 DDL/마이그레이션을 확인한 뒤 A(권장) 또는 B를 적용하라.src/main/java/konkuk/chacall/domain/user/domain/model/Role.java (1)
7-10: 운영 DB 마이그레이션 존재 여부 확인 필요NON_SELECTED 역할 추가는 적절합니다. 다만 저장소에서 users.role 컬럼 길이(15) 확장 또는 enum 값 추가를 반영한 마이그레이션/DDL을 찾지 못했습니다 (src/main/resources 기준으로 검색했으나 결과 없음).
- 마이그레이션이 있다면 위치를 알려주시거나(예: src/main/resources/db/migration, src/main/resources/db/changelog, 프로젝트 루트의 db/migration 또는 별도 infra 레포) users.role 변경을 적용하는 SQL/마이그레이션 파일을 커밋하세요.
- 프로덕션에서 spring.jpa.hibernate.ddl-auto가 validate 또는 none인지 확인하세요; 저장소에서 'ddl-auto' 항목이 없습니다.
| String kakaoId = oAuth2UserDetails.getProvider() + "_" + oAuth2UserDetails.getProviderId(); //kakao_1234567890 | ||
| String email = oAuth2UserDetails.getEmail(); | ||
| String nickname = oAuth2UserDetails.getNickname(); | ||
| String profileImage = oAuth2UserDetails.getProfileImage(); | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
카카오 이메일/프로필 이미지 Null 가능성 — 엔티티 제약(해당 칼럼 NOT NULL, email UNIQUE)과 충돌
- Kakao는 account_email 권한 미동의 시 이메일이 제공되지 않습니다. profile_image도 비공개/미설정일 수 있습니다.
- 현재 User.email/profileImageUrl가 NOT NULL이므로 save 시 제약 위반 가능성이 큽니다(운영 장애).
대응 옵션:
- 옵션 A(권장): User 엔티티에서 email, profileImageUrl nullable 허용 및 마이그레이션 반영. 이메일은 유니크 유지(다중 NULL 허용 DB 확인 필요).
- 옵션 B: 저장 전 애플리케이션에서 안전한 대체값을 주입(예: email 미제공 시 kakaoId 기반 내부용 placeholder). 장기적으로는 A와 병행 권장.
옵션 A 적용 diff(User.java):
- @Column(nullable = false)
+ @Column(nullable = true)
private String profileImageUrl;
@@
- @Column(length = 50, nullable = false, unique = true)
+ @Column(length = 50, nullable = true, unique = true)
private String email;옵션 B 적용 예(서비스 저장 분기):
- String email = oAuth2UserDetails.getEmail();
- String nickname = oAuth2UserDetails.getNickname();
- String profileImage = oAuth2UserDetails.getProfileImage();
+ String email = oAuth2UserDetails.getEmail();
+ String nickname = oAuth2UserDetails.getNickname();
+ String profileImage = oAuth2UserDetails.getProfileImage();
+ if (email == null || email.isBlank()) {
+ email = kakaoId + "@placeholder.kakao";
+ }
+ if (profileImage == null) {
+ profileImage = "";
+ }카카오 이메일 제공이 선택 동의인지 최신 문서를 확인해 주세요:
원하시면 마이그레이션 스크립트 템플릿도 제공하겠습니다.
Also applies to: 53-56
🌐 Web query:
Is Kakao OAuth2 "account_email" scope optional, and can email be absent in the user info response?
💡 Result:
Short answer: Yes. The account_email scope is not guaranteed — it can be optional or unavailable (e.g., requires business verification), and the returned user object may have no email even when you request it (user has no email, revoked consent, skipped consent, or your app isn't allowed to request that scope). See Kakao docs and developer reports. [1][2][3]
Sources:
- Kakao FAQ: email can be empty; check consent settings and whether user/account has email. [1]
- Kakao REST API docs: how to request account_email and related fields; shows email-related flags and that consent/state can make email absent. [2]
- Reports/docs noting account_email may require business app verification (scope unavailable to some apps). [3]
중대: Kakao OAuth2는 이메일/프로필이 없을 수 있으므로 User.email/profileImageUrl NOT NULL 제약으로 인한 DB 예외 위험 — 즉시 조치 필요
웹 확인: Kakao의 account_email scope는 선택적/미제공 가능하며 사용자 동의·앱권한 등에 따라 이메일이 응답에 없을 수 있음.
- 위치: src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java (라인 43–47, 동일 동작 53–56).
- 권장(우선): Option A — User 엔티티에서 email, profileImageUrl을 nullable로 변경하고 DB 마이그레이션 적용(이메일 UNIQUE 제약은 DB에서 NULL 다중 허용 여부 확인).
- 대안(임시): Option B — 저장 전 안전한 대체값 주입(예: 이메일이 없으면 kakaoId + "@placeholder.kakao", profileImage가 없으면 빈값).
옵션 A 적용 diff(User.java):
- @Column(nullable = false)
+ @Column(nullable = true)
private String profileImageUrl;
@@
- @Column(length = 50, nullable = false, unique = true)
+ @Column(length = 50, nullable = true, unique = true)
private String email;옵션 B 적용 예(서비스 저장 분기):
- String email = oAuth2UserDetails.getEmail();
- String nickname = oAuth2UserDetails.getNickname();
- String profileImage = oAuth2UserDetails.getProfileImage();
+ String email = oAuth2UserDetails.getEmail();
+ String nickname = oAuth2UserDetails.getNickname();
+ String profileImage = oAuth2UserDetails.getProfileImage();
+ if (email == null || email.isBlank()) {
+ email = kakaoId + "@placeholder.kakao";
+ }
+ if (profileImage == null) {
+ profileImage = "";
+ }📝 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 kakaoId = oAuth2UserDetails.getProvider() + "_" + oAuth2UserDetails.getProviderId(); //kakao_1234567890 | |
| String email = oAuth2UserDetails.getEmail(); | |
| String nickname = oAuth2UserDetails.getNickname(); | |
| String profileImage = oAuth2UserDetails.getProfileImage(); | |
| String kakaoId = oAuth2UserDetails.getProvider() + "_" + oAuth2UserDetails.getProviderId(); //kakao_1234567890 | |
| String email = oAuth2UserDetails.getEmail(); | |
| String nickname = oAuth2UserDetails.getNickname(); | |
| String profileImage = oAuth2UserDetails.getProfileImage(); | |
| if (email == null || email.isBlank()) { | |
| email = kakaoId + "@placeholder.kakao"; | |
| } | |
| if (profileImage == null) { | |
| profileImage = ""; | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/global/common/security/oauth2/CustomOAuth2UserService.java
around lines 43–47 (and similar behavior at 53–56), Kakao may return no email or
profileImage which will cause DB NOT NULL constraint failures; either make
User.email and User.profileImageUrl nullable and apply a DB migration (Option
A), ensuring unique/email constraints behave with NULLs, or implement a safe
fallback before persisting (Option B) by injecting deterministic placeholder
values (e.g., kakaoId + "@placeholder.kakao" for missing email and an empty
string or default image URL for missing profileImage) and document the choice;
update unit/integration tests and any validation logic accordingly.
#️⃣연관된 이슈
📝작업 내용
카카오 소셜 로그인을 도입했습니다. 동작 흐름은 다음과 같습니다.
/oauth2/authorization/kakao로 요청 (이때 redirect_uri를 쿼리파라미터로 어떤 도메인에 리다이렉트를 받을것인지 전달) ex:http://localhost:8080/oauth2/authorization/kakao?redirect_uri=http://localhost:5173토큰 발급 API로 요청스크린샷
리다이렉트 직후

발급받은 토큰 키로 액세스 토큰 발급

💬리뷰 요구사항(선택)
동적 리다이렉트를 구현하느라 시간이 조금 걸렸습니다..
테스트를 위해서 임시로 index.html 정적 파일과 엔드포인트를 허용해두었습니다. 추후에 삭제하도록 하겠습니다.
로컬에서 테스트해보고 싶으시면, http://localhost:8080/oauth2/authorization/kakao?redirect_uri=http://localhost:8080으로 접속하시면 됩니다!
또 현재 AuthParameters에서 각 일반유저, 사장님 뷰의 엔드포인트를 아직 몰라서 임의로 설정해두었습니다. 추후에 프론트 개발자 분들께 여쭤보고 수정하겠습니다.
Summary by CodeRabbit