[Spring Boot] Spring Security JWT 토큰 로그인 구현 - 1에 이어서 로그인 요청 시 인증 수행 과정을 구현하자.

 

 

CustomDetailsService 구현

// Spring Security의 UserDetailsService
public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

// 비밀번호를 추가로 받는 CustomDetailsService
public interface CustomDetailsSerivce {
    UserDetails loadUserByUsername(String username, String password) throws UsernameNotFoundException;
}

먼저 Spring Security의 UserDetailsService의 loadUserByUsername 메소드는 username만을 파라미터로 받기 때문에 추가로 password를 파라미터로 받는 CustomDetailsService 인터페이스를 만든다.

 

@Component
@RequiredArgsConstructor
public class CustomUserDetailsService implements CustomDetailsSerivce {

    private final PasswordEncoder passwordEncoder;
    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username, String password) {

        if (StringUtils.isEmpty(username)) {
            throw new UsernameNotFoundException("ID를 입력해주세요.");
        }
        if (StringUtils.isEmpty(password)) {
            throw new BadCredentialsException("Password를 입력해주세요.");
        }

        Member member = memberRepository.findByUserId(username)
                .orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 Member입니다."));

        if (!passwordEncoder.matches(password, member.getPassword())) {
            throw new BadCredentialsException("Password가 일치하지 않습니다.");
        }

        // 임시로 USER 권한 부여
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));

        User user = new User(
                member.getUserName(),
                "",
                grantedAuthorities
        );

        return user;
    }

Spring Security Config에 Bean으로 등록한 패스워드인코더와 사용자 정보를 저장하는 Member 테이블에 접근하는 MemberRepository 의존성을 주입받는다.

 

username, password 입력에 대한 유효성과 패스워드 일치 유효성 검증 후 성공 시 임시로 ROLE_USER 권한을 부여한 UserDetails 객체롤 리턴한다.

 

유효성 검증 실패 시 throw하는 UsernameNotFoundException, BadCredentialsException는 AuthenticationException을 상속받는 클래스들로 1편에서 구현했던 JwtAuthenticationEntryPoint에서 예외 핸들링을 하게 된다.

 

CustomAuthenticationProvider 구현

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final CustomDetailsSerivce customDetailsSerivce;
    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        UserDetails userDetails = customDetailsSerivce.loadUserByUsername((String) authentication.getPrincipal(), (String) authentication.getCredentials());

        return new UsernamePasswordAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

AuthenticationManager로부터 인증을 위임받는 AuthenticationProvider를 구현하여 CustomUserDetailsService의 loadByUsername 메소드를 수행하고 인증 객체를 리턴한다.

 

LoginService 구현

@Getter
public class LoginDto {
    private String userId;
    private String password;
}

userId와 password가 담긴 LoginDto를 파라미터로 받는 LoginService를 구현한다.

 

@Service
@RequiredArgsConstructor
public class LoginService {

    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final TokenProvider tokenProvider;

    public TokenDto login(LoginDto loginDto) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUserId(), loginDto.getPassword());
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        TokenDto tokenInfo = tokenProvider.generateToken(authentication);

        return tokenInfo;
    }
}

파라미터로 받은 LoginDto로 UsernamePasswordAuthenticationToken 객체를 생성해 authenticationManager의 인증을 수행한다. authenticationManager은 방금 구현한 CustomAuthenticationProvider에게 인증을 위임하고 인증 성공 시 SecurityContextHolder에 인증 정보를 저장하고 액세스 토큰을 발급하여 리턴한다.

 

Login API 호출하여 인증 성공 시 accessToken과 refreshToken을 응답으로 받는 것을 확인할 수 있다.

 

끝.