Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

리프레시 토큰 재발급 오류 수정 및 관련 코드 리팩토링, 일반 회원가입 기능 추가 #61

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

Okman-0920
Copy link
Collaborator

@Okman-0920 Okman-0920 commented Feb 28, 2025

📌 과제 설명

  1. 추가기능 개발을 위해 일반 회원가입 및 로그인 기능 추가
  2. 엑세스토큰 재발급 로직 오류 수정

👩‍💻 요구 사항과 구현 내용

1. 일반 회원가입 및 로그인

  • 기존 로그인은 소셜로그인만 구현되어 있어 일반로그인을 구현하였음
  • 추후 소셜 로그인은 일반 로그인 계정에 연동되도록 추가 개발 예정

2. 엑세스 토큰 재발급 오류 수정
[현황]

  • accessToken 재발급 과정에서 만료된 accessToken을 사용하여 재발하고 만료된 accessToken을 BlackList를에 보관
  • 이미 만료된 accessToken이 블랙리트스테 등록
  • 로그아웃 시도 시 만료가 안된 accessToken이 처리되지 않고 있음

[문제점]

  • 만료된 accessToken을 블랙리스트에 등록하는 것은 불필요한 데이터 저장으로 성능 저하 (DB 저장공간 낭비) 우려
  • 만료되지 않은 accessToken이 블랙리스트에 등록되지 않을 경우 재사용될 우려가 있음

[해결방안]
a. accessToken 재발급 로직 수정

  • accessToken 만료시 서버에서 클라이언트에 401오류 반환
  • 클라이언트는 /auth/refresh 엔드포인트로 재발급을 요청 + 리프레시토큰이 담긴 쿠키 전달
  • 서버에서 쿠키의 JWT 추출, redis에 저장된 refreshToken과 비교
  • 일치여부 확인 후 accessToken 발급 및 쿠키로 전달
  • 만약 refreshToken또한 만료 되었다면 강제 로그아웃 후 다시 로그인을 요청

b. 만료 안된 accessToken 처리

  • 로그아웃을 하는 순간 accessToken이 만료되지 않으면 blackList에 등록
  • refreshToken역시 만료되지 않으면 redis에서 바로 삭제하도록 처리
  • 클라이언트에겐 빈 쿠키 전달

3. 기타 관련 코드 리팩토링
a. CookieUtil.java,

  • accessToken, refreshToken 추출 로직 구분
  • 토큰을 추출하는 공통 메서드 작성

b. JwtAuthenticationFilter.java

  • 쿠키메서드 변경사항 반영

c. authController 및 authService 레이어 역할에 따른 코드 리팩토링

  • controller와 service의 책임을 명확하게 분리

d. 코드 가독성을 위해 refreshToken 검증 로직과, 삭제 로직을 분리

✅ 피드백 반영사항

  1. accessToken 재발급 시 만료된 토큰을 사용하여 재발급 하는ㄴ 로직 수정

✅ PR 포인트 & 궁금한 점

@Okman-0920 Okman-0920 linked an issue Feb 28, 2025 that may be closed by this pull request
@Okman-0920 Okman-0920 self-assigned this Feb 28, 2025
Copy link
Collaborator

@dnzp75 dnzp75 left a comment

Choose a reason for hiding this comment

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

네이버 컨벤션 스타일 적용되어있는지 확인 부탁드립니다.

고생하셨습니다

Comment on lines +42 to +46
@PostMapping("/signup")
@Operation(summary = "일반 회원가입")
public ResponseEntity<String> signUp(@Valid @RequestBody SignUpRequestDTO signUpRequestDTO) {
authService.signUp(signUpRequestDTO);
return ResponseEntity.status(HttpStatus.CREATED).body("회원가입이 완료되었습니다.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

멘토님이 리뷰해주신 내용이 있습니다.
응답에 대해서는 꼭 필요한 데이터만 반환하는 정책을 많은 기업들도 사용하고 있다고 합니다.
프론트에게 필요하지 않는 데이터는 굳이 안보내줘도 될 것 같습니다 Ex) "회원가입 완료되었습니다"
그 외 API들도 공통적입니다.

#45 (comment)

public ResponseEntity<String> login(
@Valid @RequestBody LoginRequestDTO loginRequestDTO,
HttpServletResponse response) {
log.debug("로그인 시도 - email: {}", loginRequestDTO.email());
Copy link
Collaborator

@dnzp75 dnzp75 Feb 28, 2025

Choose a reason for hiding this comment

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

이 로그는 꼭 필요한 Log라서 작성하신건가요?

그 외에 다른 클래스들에서도 쓰인 Log들도 어떠한 목적이 있어서 사용하신건가요? 당장은 괜찮지만 후에 무분별한 log사용은 성능 저하의 원인이 된다고 합니다

Comment on lines +56 to +73
try {
// 로그인 처리 및 토큰 발급
Map<String, String> tokens = authService.login(loginRequestDTO);

// Access Token 쿠키 설정
ResponseCookie accessTokenCookie = cookieUtil.createAccessTokenCookie(tokens.get("accessToken"));
ResponseCookie refreshTokenCookie = cookieUtil.createRefreshTokenCookie(tokens.get("refreshToken"));

response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString());
response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString());

log.debug("로그인 성공 - email: {}", loginRequestDTO.email());
return ResponseEntity.status(HttpStatus.OK).body("로그인 성공");

} catch (Exception e) {
log.error("로그인 실패 - email: {}, error: {}", loginRequestDTO.email(), e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
Copy link
Collaborator

@dnzp75 dnzp75 Feb 28, 2025

Choose a reason for hiding this comment

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

AccessToken도 쿠키로 만들어서 보내주는 이유가 있나요?

Comment on lines +42 to +57
public void logout(HttpServletRequest request, HttpServletResponse response) {
log.info("로그아웃 요청 수신");

String logoutRefreshToken = cookieUtil.extractRefreshTokenFromCookie(request);
String logoutAccessToken = cookieUtil.extractAccessTokenFromCookie(request);

try {
// 1. RefreshToken에서 사용자 ID 추출
Long userId = jwtTokenProvider.getUserIdFromRefreshToken(logoutRefreshToken);
log.debug("로그아웃 userId: {}" , userId);

public String refreshAccessToken(String accessToken) {
// 1. Access Token 블랙리스트 체크
validateAccessToken(accessToken);
// 2. AccessToken 이 유효하면 블랙리스트 추가
if (jwtTokenProvider.validateToken(logoutAccessToken)) {
jwtTokenProvider.addToBlacklist(logoutAccessToken);
log.info("AccessToken 블랙리스트에 추가 완료 - userId: {}", userId);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

블랙 리스트에 등록된 accessToken은 계속 redis에 남아있는건가요?

Comment on lines +79 to +84
@Transactional
public void signUp(SignUpRequestDTO signUpRequestDTO) {
// 중복 검사
if (memberRepository.findByEmail(signUpRequestDTO.email()).isPresent()) {
throw new JwtAuthenticationException("이미 가입된 이메일입니다.");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

일반 로그인한 사용자와 소셜 로그인한 사용자의 이메일이 같은 경우에는 어떻게 처리되나요?

Comment on lines +86 to +92
Member member = new Member(
signUpRequestDTO.name(),
signUpRequestDTO.email(),
passwordEncoder.encode(signUpRequestDTO.password()),
signUpRequestDTO.imageUrl());

// 2. 토큰에서 사용자 정보 추출
Long userId = jwtTokenProvider.getUserIdFromExpiredToken(accessToken);
memberRepository.save(member);
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 코드 관련 내용도 멘토님께서 남겨주신 내용이 있습니다.

#19 (comment)

Member member = memberRepository.findByEmail(loginRequestDTO.email())
.orElseThrow(() -> {
log.warn("로그인 실패 - 존재하지 않는 이메일: {}", loginRequestDTO.email());
return new JwtAuthenticationException("가입되지 않은 이메일입니다.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

서비스 로직에 대한 예외 처리는 CustomException을 사용하면 예외를 일관되게 관리할 수 있고, 추후 확장성 및 예외 처리 로직을 쉽게 개선할 수 있습니다.

Comment on lines -159 to +172
.issuedAt(now)
.expiration(validity)
.signWith(key)
.compact();
.subject(String.valueOf(userId))
.issuedAt(now)
.expiration(validity)
.signWith(key)
.compact();
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 네이버 컨벤션 스타일이 적용되어있는 상태인가요? 이런 현상은 컨벤션이 안맞아서 나타나는 것 같습니다.

public String refreshToken(String refreshToken) {
try {
// 1. 리프레시 토큰 유효성 검사
jwtTokenProvider.validateToken(refreshToken);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이미 필터에서 해당 토큰 유효성 검사하지 않나요?

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.

[REFACTOR] 엑세스토큰 재발급 기능 리팩토링
2 participants