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

[feat] 사용자 로그인 기능 구현 #22

Merged
merged 16 commits into from
Aug 26, 2024
Merged

[feat] 사용자 로그인 기능 구현 #22

merged 16 commits into from
Aug 26, 2024

Conversation

jw427
Copy link
Contributor

@jw427 jw427 commented Aug 25, 2024

Issue

PR 타입(하나 이상의 PR 타입을 선택해주세요)

  • 기능 추가
  • 기능 삭제
  • 버그 수정
  • 의존성, 환경 변수, 빌드 관련 코드 업데이트

반영 브랜치

feat/login -> dev

변경 사항

  • jwt사용을 위한 dependency와 Spring Security 설정을 위한 Config 파일 수정
  • 로그인 시 accessTokenrefreshToken 발급
  • 토큰 만료시 accessTokenrefreshToken 재발급

테스트 결과

로그인

Request

HTTP : `POST`
URL: /user/login
  • Request Body
{
  "account": "계정",
  "password": "비밀번호"
}

Response : 성공시

{
  "userId": "회원 고유아이디",
  "token": "액세스 토큰"
}

토큰 재발급

Request

HTTP : `POST`
URL: /api/token
  • Request Header
Authorization: “Bearer XXXXXXXXX”
  • Request Body
{
  "accessToken": "액세스 토큰",
  "refreshToken": "리프레시 토큰"
}

Response : 성공시

{
  "accessToken": "재발급 받은 액세스 토큰",
  "refreshToken": "재발급 받은 리프레시 토큰"
}

jw427 added 2 commits August 24, 2024 01:22
- accessToken을 이용한 로그인 기능 먼저 구현
- UserDetail과 UserDetailService 생성
- UserRepository 작성
- 토큰 생성, 유효성 검증, 토큰에서 필요한 정보 가져오는 TokenProvider 클래스 생성
- 발급받은 토큰을 검증하고 다음 필터로 전달하는 TokenAuthenticationFilter 클래스 생성
- Spring Security 설정을 위한 SecurityConfig 작성
- UserController 작성
- 기존의 로그인시 액세스토큰만 발급되던 코드에 리프레시토큰도 발급되어 저장되는 로직 UserService에 추가
- 로그인해도 토큰이 바뀌지 않는 문제 TokenProvider에서 수정
-- claim에 발급 시간과 만료 시간 추가
- TokenService에서 리프레시 토큰을 검증해 액세스토큰과 리프레시토큰 재발급하는 로직 추가
@jw427 jw427 added the feat label Aug 25, 2024
@jw427 jw427 self-assigned this Aug 25, 2024
Copy link
Contributor

@pie2457 pie2457 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~ 리뷰 읽어보시고 반영 부탁드려요 ㅎㅎ

@Value("${jwt.secret_key}")
private String key;

private long tokenValidTime = 1000L * 60 * 60; // 1시간
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분은 secret.yml에 같이 넣어도 될 것 같아요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

토큰 만료 기간 말하는 걸까요?

Copy link
Contributor

@pie2457 pie2457 Aug 25, 2024

Choose a reason for hiding this comment

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

네! 그리고 .yml 파일들 전부 노션에 올려주세요 ~

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yml 파일은 노션 문서 페이지 하단에 올려두었습니다. 빈 값들은 각자 로컬 환경에 맞게 넣어서 사용하시면 될 것 같아요!

Date now = new Date();
long time = type.equals("access") ? tokenValidTime : RefreshTokenValidTime;

return Jwts.builder()
Copy link
Contributor

Choose a reason for hiding this comment

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

        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입
                .setClaims(Jwts.claims()
                            .setSubject(account)
                            .setAudience(type)
                            .setIssuedAt(now)
                            .setExpiration(new Date(now.getTime() + time))
                          )
                .signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화
                .compact();
    }

메서드 체이닝 방식은 .을 기준으로 내려주시면 가독성이 더 좋은 것 같습니다~!

import java.util.UUID;

public interface TokenRepository extends JpaRepository<Token, Long> {
@Query("SELECT t FROM Token t WHERE t.user.userId = :userId")
Copy link
Contributor

Choose a reason for hiding this comment

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

여기는 Query를 작성하신 이유가 있나용?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

메서드 네이밍 문제인지 회원고유식별값으로 해당 회원의 토큰을 찾는 동작을 제대로 수행하지 못해 쿼리를 작성하게 됐습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

왜 userId를 못찾아오는지 확인해보셔도 될 것 같아요 원래라면 되야 하는 메서드니까~!

String accessToken = tokenProvider.makeToken(user.getAccount(), "access");
String refreshToken = tokenProvider.makeToken(user.getAccount(), "refresh");

storedToken.updateToken(refreshToken);
Copy link
Contributor

Choose a reason for hiding this comment

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

updateToken메서드를 여기서만 사용하시는 거라면 updateToken 메서드의 return 타입은 void여도 괜찮을 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

UserService 클래스에서 새 토큰으로 업데이트할 때도 사용하고 있습니다!

@@ -11,4 +16,10 @@
public class UserController {

private final UserService userService;

@PostMapping("/login")
public ResponseEntity<UserLoginResponseDto> loginUser(@RequestBody UserLoginRequestDto requestDto) {
Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 User라는 말은 굳이 사용하지 않으셔도 괜찮을 것 같아요 ! 로그인 기능이니까 ㅎㅎ

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class UserLoginRequestDto {

Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 필드 공백 제거해주세요!

@Service
@RequiredArgsConstructor
public class TokenService {

Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 필드 공백 제거해주세요!

@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
User user = userRepository.findByAccount(account)
.orElseThrow(() -> new IllegalArgumentException("계정이 존재하지 않습니다."));
Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 NotFound가 맞을 것 같네요~!

import java.util.Collection;
import java.util.List;

public class UserDetail implements UserDetails {
Copy link
Contributor

Choose a reason for hiding this comment

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

이 클래스는 뭐하는 클래스 인가요?
사용하지 않는 메서드를 Override할 필요가 없을 것 같은데..!
필드 공백도 제거 해주세요 ㅎㅎ

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Spring Security를 이용한 로그인을 구현하는데 있어서 Spring Security에서 사용자 정보를 담는 인터페이스인 UserDetails를 구현할 필요가 있었습니다. 해당 인터페이스의 기본 오버라이드 메서드들을 구현하느라 코드가 길어지게 됐어요. 필드 공백은 제거하도록 하겠습니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

음 .. 그럼 다른 방법이 없을까요? 기본 메서드를 전부 사용하지 않는데 구현해 놓는 것은 SOLID 원칙에도 위배가 될 것 같아요 !

Copy link
Contributor

Choose a reason for hiding this comment

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

흐음 이거에 대해서 저도 찾아보는 중인데 어쩔 수 없나보네요ㅠㅠ secrurity 너무 어렵네요..!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

UserDetails 구현을 위해서 메서드들을 사용하지 않더라도 확장성 측면에서 기본적으로 구현하는 것이 낫겠다고 판단해서 메서드들을 구현해놨는데요, 이 부분은 다른 방법을 생각해보지 못해서 고민해보겠습니다.


public interface TokenRepository extends JpaRepository<Token, Long> {
@Query("SELECT t FROM Token t WHERE t.user.userId = :userId")
Optional<Token> findByUserId(@Param("userId") UUID userID);
Copy link
Contributor

Choose a reason for hiding this comment

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

@Param 제거하시고 userId로 변경하시면 될 것 같습니당

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class TokenRequestDto {
Copy link
Contributor

Choose a reason for hiding this comment

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

이거는 record 클래스 알아보시고 사용하시면 될 것 같아요~

public record TokenRequestDto(String accessToken, String refreshToken) {
}

이렇게 간결하게 만들어줄 수 있습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네 한번 찾아보겠습니다!

import java.util.UUID;

public interface TokenRepository extends JpaRepository<Token, Long> {
@Query("SELECT t FROM Token t WHERE t.user.userId = :userId")
Copy link
Contributor

Choose a reason for hiding this comment

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

왜 userId를 못찾아오는지 확인해보셔도 될 것 같아요 원래라면 되야 하는 메서드니까~!

Optional<Token> refreshToken = tokenRepository.findByUserId(user.getUserId()); // 리프레시 토큰 있는지 확인
String newRefreshToken = tokenProvider.makeToken(requestDto.getAccount(), "refresh"); // 새 리프레시 토큰
if (refreshToken.isPresent()) { // 리프레시 토큰 있을 경우
tokenRepository.save(refreshToken.get().updateToken(newRefreshToken)); // 새 토큰으로 업데이트
Copy link
Contributor

Choose a reason for hiding this comment

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

jpa의 변경 감지 기능을 아시나용? 이거는 따로 save해주지 않아도 됩니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이 부분은 생각도 못했네요 감사합니다!

private final TokenRepository tokenRepository;
private final UserRepository userRepository;

// 액세스 토큰 재발행
Copy link
Contributor

Choose a reason for hiding this comment

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

Service 레이어에 전부 @Transaction이 빠져있네요!

- TokenProvider의 makeToken 메서드 가독성을 위한 수정
- 회원이 존재하지 않는 경우의 에러 핸들링 추가
return e.getClaims().getSubject();
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

7같은 매직넘버보다는 위의 "Bearer "를 BEARER_PREFIX같은 상수로 빼서 .length()를 사용하셔도 좋을 것 같아요!

@jw427 jw427 linked an issue Aug 25, 2024 that may be closed by this pull request
4 tasks
Copy link
Contributor

@K-0joo K-0joo 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
Contributor

@pie2457 pie2457 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
Contributor

@rhaehf rhaehf 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
Contributor

@jeongeungyeong jeongeungyeong left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!

@jw427 jw427 merged commit c78da78 into dev Aug 26, 2024
@jw427 jw427 deleted the feat/login branch August 26, 2024 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

사용자 로그인 (API)
6 participants