Skip to content

[FEAT] 카카오 소셜 로그인 연동 및 인증/인가 필터 도입#6

Merged
buzz0331 merged 42 commits into
developfrom
feat/#4-kakao-oauth-login
Sep 15, 2025
Merged

[FEAT] 카카오 소셜 로그인 연동 및 인증/인가 필터 도입#6
buzz0331 merged 42 commits into
developfrom
feat/#4-kakao-oauth-login

Conversation

@buzz0331

@buzz0331 buzz0331 commented Sep 14, 2025

Copy link
Copy Markdown
Contributor

#️⃣연관된 이슈

closes #4

📝작업 내용

카카오 소셜 로그인을 도입했습니다. 동작 흐름은 다음과 같습니다.

  1. 클라이언트가 href로 /oauth2/authorization/kakao로 요청 (이때 redirect_uri를 쿼리파라미터로 어떤 도메인에 리다이렉트를 받을것인지 전달) ex: http://localhost:8080/oauth2/authorization/kakao?redirect_uri=http://localhost:5173
  2. 쿼리 파라미터에 담긴 redirect_uri를 세션에 임시저장
  3. 카카오에서 유저 정보 조회 후 현재 DB에 존재하는지 여부 확인 (존재할 경우, 기존 정보로 LoginUser 생성 / 존재하지 않을 경우, User 테이블에 추가후 LoginUser 생성)
  4. 로그인 성공 후, SuccessHandler에서 2번에서 저장해둔 redirect_uri와 3번에서 생성한 LoginUser를 조회하여 각 유저 역할에 따라 알맞은 경로로 리다이렉트 (이때, 랜덤한 UUID를 생성하여 LoginTokenStorage에 임시 로그인 토큰 키를 저장하고, 리다이렉트 할때 쿼리파라미터로 전달)
  5. 클라이언트는 리다이렉트를 받아서 쿼리파라미터에서 loginTokenKey를 꺼내서 토큰 발급 API로 요청
  6. 토큰 발급 API에서는 전달받은 키를 이용하여 Redis에 저장된 토큰을 가져옵니다. (TTL은 5분)

스크린샷

리다이렉트 직후
스크린샷 2025-09-14 오후 3 06 39

발급받은 토큰 키로 액세스 토큰 발급
스크린샷 2025-09-14 오후 3 07 09

💬리뷰 요구사항(선택)

동적 리다이렉트를 구현하느라 시간이 조금 걸렸습니다..

테스트를 위해서 임시로 index.html 정적 파일과 엔드포인트를 허용해두었습니다. 추후에 삭제하도록 하겠습니다.
로컬에서 테스트해보고 싶으시면, http://localhost:8080/oauth2/authorization/kakao?redirect_uri=http://localhost:8080으로 접속하시면 됩니다!

또 현재 AuthParameters에서 각 일반유저, 사장님 뷰의 엔드포인트를 아직 몰라서 임의로 설정해두었습니다. 추후에 프론트 개발자 분들께 여쭤보고 수정하겠습니다.

Summary by CodeRabbit

  • New Features
    • 카카오 소셜 로그인(OAuth2) 추가
    • JWT 기반 인증 도입 및 액세스 토큰 발급 API(/auth/token) 제공
    • 로그인 리디렉션 처리 및 허용 도메인 화이트리스트 지원
    • 사용자 역할 미선택(NON_SELECTED) 상태 플로우 추가
  • Chores
    • Redis 연동으로 로그인 토큰 임시 저장
    • JSON/JavaTime 직렬화 설정 추가
    • 보안 관련 의존성 및 환경설정 값 추가
    • JSON 파싱 오류에 대한 표준화된 에러 응답 처리
  • Tests
    • 테스트 프로파일(test) 활성화

@coderabbitai

coderabbitai Bot commented Sep 14, 2025

Copy link
Copy Markdown

Walkthrough

Spring Security, OAuth2(Kakao), JWT, Redis 기반 인증 흐름을 도입. 보안 설정, JWT 필터/엔트리포인트, OAuth2 사용자/성공 핸들러, 인가 요청 리졸버, 토큰 스토리지(레디스), 사용자 조회 추가, 예외/에러코드 확장, 프로퍼티/의존성/JSON/Redis 설정 및 테스트 프로파일 반영.

Changes

Cohort / File(s) Summary
Build & Dependencies
build.gradle
Spring Security, OAuth2 Client, JWT(jjwt), Redis, Jackson 모듈 추가·활성화; 보안 테스트 의존성 활성화.
Security Core Config
src/main/java/konkuk/chacall/global/config/SecurityConfig.java, .../RedisConfig.java, .../JsonConfig.java, src/main/resources/application-*.yml
무상태(JWT) 보안 설정, CORS, OAuth2 로그인 커스터마이징, RedisTemplate/연결팩토리/직렬화, ObjectMapper(JavaTime) 빈 추가, Kakao OAuth2 및 JWT/도메인 허용 설정 추가.
JWT Pipeline
.../security/filter/JwtAuthenticationFilter.java, .../security/filter/JwtAuthenticationEntryPoint.java, .../security/util/JwtUtil.java, .../security/annotation/UserId.java, .../security/resolver/UserIdArgumentResolver.java, .../security/constant/AuthParameters.java
JWT 추출/검증/컨텍스트 주입, 인증 진입점 위임, 토큰 생성/파싱 유틸, 컨트롤러 파라미터 @userid 주입 리졸버, 인증 관련 상수 추가.
OAuth2 Login Flow
.../security/oauth2/CustomOAuth2UserService.java, .../security/oauth2/CustomSuccessHandler.java, .../security/oauth2/CustomOAuth2User.java, .../security/oauth2/LoginUser.java, .../security/oauth2/OAuth2UserDetails.java, .../security/oauth2/KakaoUserDetails.java, .../security/resolver/CustomAuthorizationRequestResolver.java, .../security/property/ServerWebProperties.java
Kakao 전용 사용자 로드/파싱, 로그인 성공 시 리다이렉트·JWT 발급·임시 로그인키 저장, OAuth2 사용자 래퍼, 리다이렉트 URI 세션 저장, 허용 오리진 검증.
Login Token Exchange API
.../security/oauth2/auth/AuthController.java, .../security/oauth2/auth/dto/AuthTokenRequest.java, .../security/oauth2/auth/dto/AuthTokenResponse.java
임시 로그인키를 JWT로 교환하는 /auth/token 엔드포인트 추가.
Login Token Storage (Redis)
.../security/oauth2/tokenstorage/LoginTokenStorage.java, .../security/oauth2/tokenstorage/RedisLoginTokenStorage.java
일회성 로그인 토큰 저장/소비 인터페이스 및 Redis 구현 추가.
Exception & Error Codes
.../exception/JsonParsingException.java, .../exception/code/ErrorCode.java, .../exception/handler/GlobalExceptionHandler.java
JSON_PARSING_ERROR, AUTH_WEB_ORIGIN_EMPTY 코드 추가; JsonParsingException 유형 및 전역 핸들러 추가(중복 메서드 존재).
Domain Model Updates
.../domain/user/domain/model/Role.java, .../domain/user/domain/model/User.java, .../domain/user/domain/repository/UserRepository.java
Role에 NON_SELECTED 추가; User 빌더/정적 팩토리(createNewUser) 및 Role 컬럼 길이 확장; Repository에 @repositoryfindByKakaoId 추가.
Tests & Profiles
src/test/java/.../ChacallApplicationTests.java, src/test/resources/application-test.yml
테스트 프로파일 활성화 및 테스트용 Redis/OAuth2/JWT 설정 추가.

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
Loading
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가 전역 예외 처리기로 위임)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

귀 댕댕, 로그인 바람을 탄다
카카오 별 따라 폴짝! 토큰을 안고
레디스 구름에 살짝 숨겨두고
열쇠 건네면 JWT 번쩍—문이 열린다
깡총깡총, 새 길 위로 오늘도 보안 좋아요 🐇🔐

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "[feat] 카카오 소셜 로그인 연동 및 인증/인가 필터 도입"은 PR의 핵심 변경사항인 카카오 OAuth2 연동과 인증/인가 관련 필터·구성 도입을 간결하고 명확하게 요약하고 있어 적절합니다. 불필요한 파일 목록이나 모호한 표현 없이 주된 목적을 전달하므로 히스토리 스캔 시 이해하기 쉽습니다.
Linked Issues Check ✅ Passed 연결된 이슈 [#4]의 요구사항인 "카카오 소셜 로그인 구현"에 대해 PR은 OAuth2 클라이언트 설정, Kakao 사용자 파싱 및 가입/조회 로직(CustomOAuth2UserService, KakaoUserDetails, User.createNewUser), 리다이렉트 보관·복원(CustomAuthorizationRequestResolver, CustomSuccessHandler), 일회성 로그인 토큰 저장/소비(RedisLoginTokenStorage, /auth/token 엔드포인트) 등을 구현해 목표 흐름을 코드로 충실히 반영하고 있습니다. 다만 원문 이슈의 체크리스트가 비어 있어 추가적인 단위/통합 테스트와 보안 검토(예: loginTokenKey 탈취 위험 완화)가 후속으로 필요합니다.
Out of Scope Changes Check ✅ Passed 대부분 변경사항은 카카오 로그인 및 인증/인가 인프라와 직접 연관되어 PR 목적에 부합하지만, GlobalExceptionHandler에 jsonParsingExceptionHandler가 중복으로 삽입된 점과 일부 불필요한 또는 사용되지 않는 import(예: API_INVALID_PARAM) 및 와일드카드 Lombok import 적용 등은 기능적 요구와 직접 관련 없는 사소한 변경 또는 정리 필요 항목으로 보입니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#4-kakao-oauth-login

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: 0
src/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: false

Also 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6d2b287 and 203b4dc.

📒 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행)
  • 확인 항목:
    1. 필터/인터셉터 등에서 request.setAttribute(JWT_ACCESS_TOKEN_KEY.getValue(), …) 호출이 실제로 존재하는지
    2. 저장되는 값의 실제 타입(예: Long/Integer/String/사용자 객체 등)
    3. 값이 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)' --hidden
src/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' . || true
src/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: 핵심 플로우 LGTM

JWT 검증 → 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"는 발견되지 않았습니다.

Comment thread build.gradle
Comment on lines +47 to +49
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment thread build.gradle
Comment on lines +52 to +53
implementation 'org.springframework.security:spring-security-oauth2-client'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +9 to +12
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByKakaoId(String kakaoId);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.
    

Comment on lines +24 to +29
Exception e = (Exception) request.getAttribute("exception");
if(e == null){
e = authException;
}
resolver.resolveException(request, response, null, e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment thread src/main/java/konkuk/chacall/global/config/RedisConfig.java
Comment thread src/main/resources/application-dev.yml
Comment thread src/main/resources/application-dev.yml
Comment thread src/main/resources/application-dev.yml Outdated
Comment thread src/test/resources/application-test.yml
ksg1227
ksg1227 previously approved these changes Sep 14, 2025

@ksg1227 ksg1227 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 어느정도 구현을 해보신 내용이다보니 전체적으로 흠 잡을 부분 없이 좋은 것 같습니다.

다만 보안적인 아쉬움이 어쩔 수 없이 생기긴 하더라구요. 이건 토큰을 사용하지 못하는 현재 상황에서는 어쩔 수 없는 문제이긴 한데, 제가 이해하기로 현재 인증 로직은

  1. 소셜 로그인 성공 시 토큰 발급을 위한 loginTokenKey 값을 리다이렉트 url 에 쿼리 파라미터로 포함시켜 반환한다.
  2. 클라이언트는 이 쿼리 파라미터에 포함된 loginTokenKey 값을 추출해서, 토큰 발급 API 에 요청을 보냄으로써 access 토큰을 발급받는다. (사실 로그인 성공시 토큰을 무조건 발급 받아야하지만, 쿠키로 발급이 현재 상황에선 힘들어서 이렇게 한 번에 depth 가 추가된 상황)

으로 이해하고있습니다.

이러한 흐름상에서 쿠키를 사용하지 않고 토큰을 발급하는 방식 중에는,

  1. 쿼리 파라미터에 access 토큰 자체를 포함시켜서 반환하는 방식
  2. 현재 방식

이 대표적인 것 같은데, 첫번째 방식은 보안적으로 따져보았을 때 access 토큰이 바로 노출되기 때문에 가장 보안성이 떨어지는 방식이라고 생각됩니다.
두번째 방식은 쿼리 파라미터에 바로 토큰을 포함시키지는 않지만, 사실상 loginTokenKey 값을 확인한 후, 토큰 발급 API 경로만 알아내서 요청을 보낸다면, 누구나 access 토큰을 발급받을 수 있는 구조인 것 같긴 합니다.

그래서 토큰이 바로 보여지냐, 한 번의 과정을 더 거쳐서 토큰을 확인하느냐 이 차이인 것 같아서, 확실히 현재 방식도 보안적인 측면에서는 다소 아쉬움이 생기네요

우선 현재로서는 해결이 어려우니까 다음에 같이 고민하고 해결 ㄱㄱ 해보시죠

고생띠하셨습니다

Comment on lines +51 to +53
.orElseGet(() -> {
User newUser = userRepository.save(User.createNewUser(nickname, profileImage, kakaoId, email));
return new CustomOAuth2User(LoginUser.createLoginUser(kakaoId, newUser.getUserId(), newUser.getRole()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에서 기존 사용자가 존재하지 않는 경우, 신규 사용자를 등록하는 로직이 존재하기 때문에, @transactional 을 메서드에 붙여주는 것도 좋을 것 같아요~


String redirectUri = request.getParameter(REDIRECT_URI_KEY.getValue());
Map<String, Object> additional = new HashMap<>(base.getAdditionalParameters());
if (StringUtils.hasText(redirectUri) && serverWebProperties.isAllowed(redirectUri)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사전에 허용된 주소인지 검증하는 과정 거치기 좋네요 굿

Comment on lines +50 to +51
request.getSession(true).setAttribute(REDIRECT_SESSION_KEY.getValue(), redirectUri);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에는 그냥 request.setAttribute 만 하면 되는 것 아닌가? 굳이 세션에 저장해야하는가? 의문점이 들어서 찾아보니, 중간에 리다이렉션을 거치다보니 세션에 저장을 하지 않으면 setAttribute 로 설정한 데이터는 날라간다고하네요, 잘 적용하신 것 같습니다 👍🏻

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고민 좀 했습니다 😆

Comment on lines +50 to +56
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 사실상 CustomAuthorizationRequestResolver 쪽의 로직과 비교해보았을 때, 중복된 로직이라고도 볼 수 있을 것 같긴 한데,
Resolver 를 통한 도메인 1차 검증, SuccessHandler 측에서의 2차 검증을 통한 안정성 강화 측면에서 작성된 로직이라고 생각하면 될까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 첫번째 Resolver에서 검증은 세션을 추가하냐 안하냐를 판단하기 위한 분기이고 두번째 SuccessHandler에서의 검증은 올바른 redirect_uri가 아니거나 redirect_uri 자체가 비었을 경우를 검증해서 저희가 정한 특정 도메인으로 redirect 되도록 하기 위한 분기입니다!

Comment on lines +38 to +41
if (loginTokenKey == null || loginTokenKey.isBlank()) {
throw new AuthException(API_INVALID_PARAM,
new IllegalArgumentException("loginTokenKey는 필수 파라미터입니다."));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 Bean Validation을 활용해서 DTO 에 @notblank 를 붙여주는 것이 더 깔끔할 것 같습니당

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵넵

Comment on lines +9 to +29
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;
}
}

@ksg1227 ksg1227 Sep 14, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분을 인터페이스로 관리하시는 이유가 있나요? Redis 를 활용하지 않고, 기존에 THIP 에서 했던 것처럼 로컬에 ConcurrentHashMap 을 만들어서 사용하는 상황, 혹은 그 외의 다른 방식으로 구현 방식을 교체하는 상황을 대비한 것인가요?

=> Entry 클래스에 대해서는 코드래빗 리뷰도 확인해볼만 할 것 같습니다. Record 를 활용해도 좋을 것 같아요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넵 처음에 Redis가 잘 동작하지 않을 것에 대비해서 LoginTokenStorage를 인터페이스로 추출하고 MemoryLoginTokenStorage와 RedisLoginTokenStorage를 나눠서 개발하였는데 Redis 방식에 훨씬 괜찮은 것 같아 하나의 방식으로만 구현하도록 결정했습니다. 인터페이스를 제거하는게 나을까요??

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 근데 어찌되었든 확장에 대한 이정도 대비는 나쁘지 않다고 생각하고, 개인적으로 Entry 클래스를 인터페이스 내부에 만들어서 사용하는 것이 좋은 것 같아서 유지해도 좋을 것 같습니다!

@buzz0331

Copy link
Copy Markdown
Contributor Author

이미 어느정도 구현을 해보신 내용이다보니 전체적으로 흠 잡을 부분 없이 좋은 것 같습니다.

다만 보안적인 아쉬움이 어쩔 수 없이 생기긴 하더라구요. 이건 토큰을 사용하지 못하는 현재 상황에서는 어쩔 수 없는 문제이긴 한데, 제가 이해하기로 현재 인증 로직은

  1. 소셜 로그인 성공 시 토큰 발급을 위한 loginTokenKey 값을 리다이렉트 url 에 쿼리 파라미터로 포함시켜 반환한다.
  2. 클라이언트는 이 쿼리 파라미터에 포함된 loginTokenKey 값을 추출해서, 토큰 발급 API 에 요청을 보냄으로써 access 토큰을 발급받는다. (사실 로그인 성공시 토큰을 무조건 발급 받아야하지만, 쿠키로 발급이 현재 상황에선 힘들어서 이렇게 한 번에 depth 가 추가된 상황)

으로 이해하고있습니다.

이러한 흐름상에서 쿠키를 사용하지 않고 토큰을 발급하는 방식 중에는,

  1. 쿼리 파라미터에 access 토큰 자체를 포함시켜서 반환하는 방식
  2. 현재 방식

이 대표적인 것 같은데, 첫번째 방식은 보안적으로 따져보았을 때 access 토큰이 바로 노출되기 때문에 가장 보안성이 떨어지는 방식이라고 생각됩니다. 두번째 방식은 쿼리 파라미터에 바로 토큰을 포함시키지는 않지만, 사실상 loginTokenKey 값을 확인한 후, 토큰 발급 API 경로만 알아내서 요청을 보낸다면, 누구나 access 토큰을 발급받을 수 있는 구조인 것 같긴 합니다.

그래서 토큰이 바로 보여지냐, 한 번의 과정을 더 거쳐서 토큰을 확인하느냐 이 차이인 것 같아서, 확실히 현재 방식도 보안적인 측면에서는 다소 아쉬움이 생기네요

우선 현재로서는 해결이 어려우니까 다음에 같이 고민하고 해결 ㄱㄱ 해보시죠

고생띠하셨습니다

이미 어느정도 구현을 해보신 내용이다보니 전체적으로 흠 잡을 부분 없이 좋은 것 같습니다.

다만 보안적인 아쉬움이 어쩔 수 없이 생기긴 하더라구요. 이건 토큰을 사용하지 못하는 현재 상황에서는 어쩔 수 없는 문제이긴 한데, 제가 이해하기로 현재 인증 로직은

  1. 소셜 로그인 성공 시 토큰 발급을 위한 loginTokenKey 값을 리다이렉트 url 에 쿼리 파라미터로 포함시켜 반환한다.
  2. 클라이언트는 이 쿼리 파라미터에 포함된 loginTokenKey 값을 추출해서, 토큰 발급 API 에 요청을 보냄으로써 access 토큰을 발급받는다. (사실 로그인 성공시 토큰을 무조건 발급 받아야하지만, 쿠키로 발급이 현재 상황에선 힘들어서 이렇게 한 번에 depth 가 추가된 상황)

으로 이해하고있습니다.

이러한 흐름상에서 쿠키를 사용하지 않고 토큰을 발급하는 방식 중에는,

  1. 쿼리 파라미터에 access 토큰 자체를 포함시켜서 반환하는 방식
  2. 현재 방식

이 대표적인 것 같은데, 첫번째 방식은 보안적으로 따져보았을 때 access 토큰이 바로 노출되기 때문에 가장 보안성이 떨어지는 방식이라고 생각됩니다. 두번째 방식은 쿼리 파라미터에 바로 토큰을 포함시키지는 않지만, 사실상 loginTokenKey 값을 확인한 후, 토큰 발급 API 경로만 알아내서 요청을 보낸다면, 누구나 access 토큰을 발급받을 수 있는 구조인 것 같긴 합니다.

그래서 토큰이 바로 보여지냐, 한 번의 과정을 더 거쳐서 토큰을 확인하느냐 이 차이인 것 같아서, 확실히 현재 방식도 보안적인 측면에서는 다소 아쉬움이 생기네요

우선 현재로서는 해결이 어려우니까 다음에 같이 고민하고 해결 ㄱㄱ 해보시죠

고생띠하셨습니다

네넵 맞아요. 우선 첫번째 방식은 쿼리 파라미터에 바로 access token이 노출되기 때문에 보안에 매우 취약하다고 알고 있습니다. 현재 구현방식 또한 상균님이 말씀하시는 것처럼 loginTokenKey가 탈취될 경우 보안적으로 문제가 생기긴 할 것 같네요. 다만, 탈취당하더라도 두가지 때문에 조금이나마 1번 방식보다는 낫다고 생각했습니다.

  1. 토큰 발급 api를 호출하더라도 cors 에러가 막힘
  2. postman 같은 플랫폼으로 요청을 날리더라도 저희는 loginTokenKey를 발급하자마자 바로 클라이언트에서 토큰 발급 api를 요청 보내도록 설계했기 때문에 Redis에서 key가 바로 삭제되어 없어짐

그래서 완벽하진 않지만, 현재 상황에서는 현실적인 차선책이라고 판단했습니다. 나중에 쿠키 기반이나 더 안전한 방식으로 개선할 수 있으면 같이 논의해보면 좋을 것 같습니다!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 203b4dc and d2e88fa.

📒 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 게이트웨이).
  • 상태: 전체 저장소 검색이 일부 필터로 스킵되어 다른 파일의 로깅 여부는 확인되지 않음 — 저장소 전체에서 민감값 로깅 유무 재검증 필요.

Comment on lines +34 to +47
@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));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

⚠️ Potential issue

토큰 응답에 캐시 방지 헤더를 반드시 추가하세요.

액세스 토큰 응답은 브라우저/중간 프록시 캐시에 저장되면 안 됩니다. 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.

Comment on lines +69 to +72
---
jwt:
secret: ${JWT_SECRET}
access-expiration: 2592000000 # 30일 No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
---
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d2e88fa and 84a0c40.

📒 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) LGTM

NON_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' 항목이 없습니다.

Comment on lines +43 to +47
String kakaoId = oAuth2UserDetails.getProvider() + "_" + oAuth2UserDetails.getProviderId(); //kakao_1234567890
String email = oAuth2UserDetails.getEmail();
String nickname = oAuth2UserDetails.getNickname();
String profileImage = oAuth2UserDetails.getProfileImage();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.

Suggested change
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.

@buzz0331 buzz0331 merged commit bce2c98 into develop Sep 15, 2025
2 checks passed
@buzz0331 buzz0331 deleted the feat/#4-kakao-oauth-login branch September 15, 2025 05:39
@buzz0331 buzz0331 changed the title [feat] 카카오 소셜 로그인 연동 및 인증/인가 필터 도입 [FEAT] 카카오 소셜 로그인 연동 및 인증/인가 필터 도입 Sep 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 카카오 소셜 로그인 구현

2 participants