Skip to content

Conversation

@rogrhrh
Copy link
Collaborator

@rogrhrh rogrhrh commented Dec 11, 2025

🔀 Pull Request

🏷 PR 타입(Type)

아래에서 이번 PR의 종류를 선택해주세요.

  • Feature (새로운 기능 추가)
  • Fix (버그 수정)
  • Refactor (기능 변화 없는 구조 개선)
  • Chore (환경 설정 / 빌드 / 기타 작업)
  • Docs (문서 작업)

🍗 관련 이슈

📝 개요(Summary)

JWT 기반 인증 시스템 MVP 구현

  • 로그인/로그아웃, 토큰 갱신, 비밀번호 재설정, 이메일 인증 기능을 구현하고 SecurityContext 기반 인증 처리로 리팩토링했습니다.

🔧 코드 설명 & 변경 이유(Code Description)

주요 구현 기능

  1. 인증/인가 시스템

    • JWT 기반 로그인/로그아웃: Access Token은 Authorization 헤더, Refresh Token은 httpOnly secure 쿠키로 전달
    • Refresh Token DB 저장 및 검증을 통한 토큰 갱신 (기존 토큰 삭제 후 신규 발급)
    • SecurityUtil을 통한 SecurityContext 기반 사용자 정보 조회로 컨트롤러 단순화
  2. 비밀번호 재설정

    • UUID 기반 토큰 생성 및 이메일 발송 (현재는 TODO로 남김)
    • 토큰 검증, 만료 확인, 사용 여부 체크 후 비밀번호 업데이트
  3. 이메일 인증

    • 6자리 숫자 인증 코드 생성 및 발송 (현재는 TODO로 남김)
    • 인증 코드 검증 후 이메일 인증 상태 업데이트
    • 이메일 인증 상태 조회 API 제공

기술적 결정

  • Refresh Token 저장: DB에 저장하여 서버 측에서 토큰 무효화 및 관리 가능
  • 쿠키 설정: httpOnly, secure, path 설정으로 XSS 및 CSRF 공격 방어
  • SecurityUtil 도입: @AuthenticationPrincipal 대신 SecurityContext 직접 조회로 의존성 감소

🧪 테스트 절차(Test Plan)

🔄 API 변경 / 흐름 영향(API & Flow Impact)

신규 API 엔드포인트

  • POST /api/v1/auth/login - 로그인
  • POST /api/v1/auth/logout - 로그아웃
  • POST /api/v1/auth/refresh - 토큰 갱신
  • POST /api/v1/auth/password/send - 비밀번호 재설정 이메일 발송
  • POST /api/v1/auth/password/reset - 비밀번호 재설정
  • POST /api/v1/auth/email/send - 이메일 인증 코드 발송
  • POST /api/v1/auth/email/verify - 이메일 인증 확인
  • GET /api/v1/auth/email - 이메일 인증 상태 조회

인증 방식 변경

  • 기존: @AuthenticationPrincipal CustomUserDetails 파라미터 주입
  • 변경: SecurityUtil.getCurrentUserIdOrThrow() 사용
  • 영향: 인증이 필요한 모든 컨트롤러에서 동일한 방식으로 사용 가능

Swagger 문서화

  • 모든 API에 상세한 응답 예시 및 에러 코드 문서화 완료

👀 리뷰 포인트(Notes for Reviewer)

  1. 로직상 문제점 확인: postman 테스트는 진행했지만 긁어온 코드가 많아 틀린점이 있을수 있습니다!
  2. 에러 처리: 각 API의 에러 응답 코드와 메시지가 일관성 있는지 확인 부탁드립니다.
  3. 보안: httpOnly, secure 쿠키 설정이 프로덕션 환경에서 올바르게 동작하는지 검토 필요합니다.

피드백 수정사항 정리

  1. AuthServiceImpl 내부 중복 코드를 정리하고, 토큰/쿠키/만료 체크 로직을 공통 메서드로 분리함, 헬퍼형태로 서비스 맨 밑으로 작성함
  2. 이메일 인증 재발송 제한 로직을 5분으로 통일하고, 주석과 실제 동작을 일치시킴
  3. Refresh Token 쿠키의 secure 설정을 yml 기반으로 제어하도록 변경함
  4. 매직넘버들 상수화
  5. 쿠키삭제 서비스로 이동
  6. 기타 중복코드 정리
  7. isexpire 만료 확인 -> 엔티티에 메서드 추가해서 검사하도록 설정함
  8. 토큰 생성 로직 한번더 중복 제거
  9. securityUtil의 중복제거
  10. securityConfig 정리

)
public class PasswordResetToken extends BaseEntity {

private Long userId;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nullable=false 명시해주시면 좋을 것 같습니다!

}

// 최근 5분 이내 발송된 인증 코드 확인 (재발송 제한)
emailVerificationRepository.findTopByUserIdOrderByCreatedAtDesc(user.getId())
Copy link
Collaborator

Choose a reason for hiding this comment

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

주석은 최근 5분 이내, 코드에는 1분으로 되어있어서 통일해주시면 좋을 듯 합니다!

}
""")))
})
public ResponseEntity<EmailStatusResponse> getEmailStatus(
Copy link
Collaborator

Choose a reason for hiding this comment

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

이메일 인증 상태 조회 같은 경우 다른 로직이 돌아가기 전에 인증이 되었는지 확인하는 부분이라 user entity에 emailVerify가 있으니 그게 true면 다음 작업으로 진행되고 false면 보통 errorcode로 이메일 인증을 먼저 해야합니다를 던지는 방식으로 처리했었습니다. 제가 생각하기엔 api를 불러서 확인 하는 일은 거의 없을 것 같습니다. 따로 부르게 된다면 각각 api를 불러야하니 비용이 들기도 하고요!

Copy link
Collaborator

Choose a reason for hiding this comment

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

저라면 userEntity에 isVerified() 같은 걸 넣어서 인증 여부를 boolean으로 반환해주게 할 것 같습니다.


public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
List<RefreshToken> findAllByUserId(Long userId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

refreshToken이 여러개 있는 경우가 있나요? findAll을 써서 다 불러와야 하는지 고려해보면 좋을것 같습니다.

// Refresh Token을 httpOnly secure 쿠키로 설정
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

secure이 항상 true면 http에선 안되서 로컬 개발 단계에선 쿠키가 아예 전송되지 않아 refresh가 작동하지 않을 수 있습니다! 그래서 dev와 prop 나눠서 설정하고 그런다 알고 있긴한데 잘 기억 안 나서 찾아보셔야 할 것 같아요!

response.setHeader("Authorization", "Bearer " + accessToken);

// Refresh Token을 httpOnly secure 쿠키로 설정
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
Copy link
Collaborator

Choose a reason for hiding this comment

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

설정 중복 코드 따로 빼는 게 좋을 것 같습니다

claims.put("userId", user.getId());
claims.put("email", user.getEmail());

String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), claims);
Copy link
Collaborator

Choose a reason for hiding this comment

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

생성 중복 코드 따로 빼는 게 좋을 것 같습니다

refreshTokenRepository.save(newRefreshTokenEntity);

// Access Token을 Response Header에 설정
response.setHeader("Authorization", "Bearer " + newAccessToken);
Copy link
Collaborator

Choose a reason for hiding this comment

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

헤더 중복 코드 따로 빼는 게 좋을 것 같습니다

String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), claims);
String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail(), claims);

// Refresh Token DB 저장
Copy link
Collaborator

Choose a reason for hiding this comment

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

저장 중복 코드 따로 빼는 게 좋을 것 같습니다

.orElseThrow(() -> new ServiceException(ErrorCode.INVALID_REFRESH_TOKEN));

// 만료 확인
if (refreshTokenEntity.getExpiredAt().isBefore(LocalDateTime.now())) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

localDateTime 말고 duration 쓰는거 추천합니다!

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

// 회원가입 251211 ahnbs
Copy link
Collaborator

Choose a reason for hiding this comment

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

오타인 걸까요?

.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 모든 요청 허용 (개발용)
Copy link
Collaborator

Choose a reason for hiding this comment

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

모든 요청에 대해 열려있어서 이 부분 하나씩 넣으셔야 할 것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

.anyRequest().permitAll() // 모든 요청 허용 (개발용)

여기 부분입니다

Copy link
Collaborator

@BackSeungBeom BackSeungBeom left a comment

Choose a reason for hiding this comment

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

확인했습니다.

Copy link
Collaborator

@hznnoy hznnoy left a comment

Choose a reason for hiding this comment

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

확인했습니다 👍

@rogrhrh rogrhrh merged commit 82e42e0 into dev Dec 12, 2025
1 check passed
@rogrhrh rogrhrh deleted the feat/#26/create-auth-domain-mvp branch December 12, 2025 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants