스프링 공부/게시판 프로젝트 만들기

[스프링] 26. MemberService : 로그인 로직 만들기

장아장 2023. 2. 8. 17:03

이번 프로젝트에서 jwt토큰을 이용해 로그인을 하게 하려고 했다.

이를 위해서 두가지 과정이 필요하다

  • 로그인 요청을 이용해 Authentication을 만든다. 
    • 로그인 요청을 validate한다
    • 요청의 아이디와 비밀번호를 가지고 UsernamePasswordAuthenticationToken을 만들어준다. 
    • 토큰에서 authentication을 뽑아온다. 
  • Authentication을 가지고 토큰을 만들고, 토큰 응답을 반환한다. 
    • TokenProdivder에서 Authentication을 가지고 TokenDto를 생성해준다. 
    • authentication에서 name(아이디)를 key, TokenDto의 refreshToken을 value로 가지는 RefreshToken객체를 만들어 저장한다. 
    • 이를 TokenResponseDto로 반환한다. 

이렇게 두가지 과정을 진행할 것이다. 

결국적으로 서버와 클라이언트 간의 관계는

 

  1. 클라이언트 : 아이디와 비밀번호 입력
  2. 서버 : 아이디와 비밀번호로 Authentication을 가져온다. 
  3. 서버 : Authentication으로 TokenReponseDto를 받는다. 
  4. 클라이언트 : 토큰을 헤더에서 받아 권한을 가지고 추가적인 서비스를 사용한다. 

정도가 될 것 같다. 

 

그러면, 일단 로그인 요청을 보내고, Authentication을 받는 과정을 만들어봐야 겠다. 

@AllArgsConstructor
@Data
public class SignInRequestDto {

    @NotNull(message = "아이디를 입력해주세요.")
    private String username;

    @NotNull(message = "비밀번호를 입력해주세요.")
    private String password;

    public UsernamePasswordAuthenticationToken toAuthentication() {
        return new UsernamePasswordAuthenticationToken(username, password);
    }
}
Dto의 인스턴스로는 아이디와 비밀번호만을 가지며, UsernamePasswordAuthenticationToken의 생성 파라미터로 둘을 넣어 생성받게 했다. 
 
private void validateSignInRequest(SignInRequestDto signInRequestDto){
    Member member = memberRepository.findByUsername(signInRequestDto.getUsername())
            .orElseThrow(MemberNotFoundException::new);
    if(!passwordEncoder.matches(signInRequestDto.getPassword(), member.getPassword())) 
        throw new PasswordNotMatchingException();
}

입력을 검증하는 로직으로는, 아이디를 이용해 멤버를 검색했을 때, 만약 존재하지 않다면 없는 회원임을 예외처리하고, 

멤버를 찾아왔을 때 비밀번호가 일치하지 않는다면, 예외처리하는 로직을 하나 더 추가했다. 

 

@Transactional
public Authentication getAuthenticationToSignIn(SignInRequestDto signInRequestDto){
    validateSignInRequest(signInRequestDto);
    UsernamePasswordAuthenticationToken authenticationToken = signInRequestDto.toAuthentication();
    return authenticationManagerBuilder.getObject().authenticate(authenticationToken);
}

이렇게 만든 validate, toAuthentication을 이용해 Authentication을 얻는 로직을 구현했다. 

AuthenticationManagerBuilder에서 getObject를 하면 AuthenticationManager를 받아온다. 

이렇게 받아온 인증 토큰을 인증 요청 객체로써 파라미터에 넣으면, 인증이 실패하지 않는다면 인증된 Authentication 객체를 가져오게 된다.

위를 해석해보았을 때, 인증은 계정이 비활성화되었거나 잠겨있을 때 예외처리된다고 되어있다.

그리고, 문제가 없을 경우 인증된 Authentication객체를 반환받는다.  

 

이제, Authentication을 받아 Token을 만드는 처리를 해야 한다. 

이를 위해, RefreshToken이라는 객체를 만들어주었다. 

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "REFRESH_TOKEN")
@Entity
@Data
@Builder
public class RefreshToken {

    @Id
    @Column(name = "RT_KEY")
    private String key;

    @Column(name = "RT_VALUE")
    private String value;

    public RefreshToken(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public RefreshToken updateValue(String token) {
        this.value = token;
        return this;
    }
}

파라미터 없는 생성자를 사용하지 않기 위해 NoArgsConstructor의 access레벨을 Protected로 해두었다. 

이렇게 하면, 외부에서 파라미터 없는 생성자가 나오지 않게 된다. 

이후, Builder를 통해 생성하도록 @Builder를 달아주었다. 

나중에 토큰 재발행을 위한 updateValue를 추가로 넣어주었다. 

 

이렇게 refreshToken을 만들고, dto를 만들어보았다. 

TokenDto는 이전에 만들어두었고, TokenResponseDto를 만들었다. 

@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TokenResponseDto {
    private String accessToken;
    private String refreshToken;

    public TokenResponseDto(TokenDto tokenDto){
        this.accessToken = tokenDto.getAccessToken();
        this.refreshToken = tokenDto.getRefreshToken();
    }
}

토큰의 응답은 tokenDto를 통해서 만들어줄 계획이므로, 파라미터로 tokenDto를 받아 생성하게 했다. 

 

이제 서비스단에서, 토큰 이용해 로그인 서비스를 마무리 지어봐야겠다. 


@RequiredArgsConstructor
@Service
public class RefreshTokenService {

    private final RefreshTokenRepository refreshTokenRepository;

    private final TokenProvider tokenProvider;

이렇게 RefreshToken을 이용하는 서비스를 분리했다. 이유는, 로그인이 정확하게 Member라는 객체를 이용하고, Member와 관련된 서비스를 담당한다고 말하기엔, 중간부턴 RefreshToken가 사용되어 분리했다. 

 

@Transactional
public TokenResponseDto createTokenDtoByAuthentication(Authentication authentication){
    TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
    RefreshToken refreshToken = RefreshToken.builder()
            .key(authentication.getName())
            .value(tokenDto.getRefreshToken())
            .build();
    refreshTokenRepository.save(refreshToken);
    return new TokenResponseDto(tokenDto);
}

이전에 만든 TokenProvider에서 generateTokenDto를 사용했다. 

Authentication을 넣어 TokenDto를 받아왔고, 

authentication.getName()을 key, tokenDto.getRefreshToken을 value로 받아왔다. 

이를 이용해 RefreshToken을 만들어 저장해두었고, tokenDto를 이용해 ToeknResponseDto를 반환시켰다. 

 

이렇게 로그인 로직을 만들었다.

만들다보니, access Token, refresh Token에 대한 개념도 정리해둬야겠다. 

정리하고, 토큰 재발행 로직에 관해서 한번 정리를 해야겠다.