Spring

SpringSecurity + CustomAuthenticationProvider 만들기

Andrew-Yun 2022. 1. 11. 15:23

Rest API로 만든 메서드에 로그인 시도를 하면 아이디와 비밀번호가 틀렸을 때 어느 부분이 틀렸는지를 알려주려 하는데, 스프링 시큐리티에서는 AuthenticationProvider 디폴트 구현체가 내부적으로 인증을 수행하여 반환한다.

따라서 아이디와 비번이 틀린 경우에 따라 오류 메시지를 다르게 하기 위해서는 AuthenticationProvider를 커스텀하여 인증을 직접 수행해야 한다.

다행히도 스프링 시큐리티에선 이러한 부분들을 구조화 해놓았기 때문에, 인터페이스만 맞추어 구현하기만 하면 된다.

내가 수행하는 부분은 4이다. AuthenticationManager가 AuthenticationProvider에게 인증 책임을 넘기게 되는데, CustomAuthenticationProvider 구현체가 대신하게 할 것이다.

@Component("userDetailsService")
@RequiredArgsConstructor
public class AccountLoginService implements UserDetailsService {
  private final AccountRepository accountRepository;

  @Override
  public UserDetails loadUserByUsername(String accountId) throws AuthenticationException {
    return accountRepository
        .findByAccountId(accountId)
        .map(user -> createUser(accountId, user))
        .orElseThrow(() -> new BadCredentialsException("등록된 아이디가 없습니다."));
  }

  private User createUser(String accountId, Account account) {
    if (!account.isActivated()) {
      throw new RuntimeException("활성화되지 않은 아이디입니다.");
    }
    List<GrantedAuthority> grantedAuthorities =
        account.getAuthority().stream()
            .map(authority -> new SimpleGrantedAuthority(authority))
            .collect(Collectors.toList());
    return new User(account.getId(), account.getPassword(), grantedAuthorities);
  }
}
@RequiredArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
  private final UserDetailsService userDetailsService;
  private final PasswordEncoder passwordEncoder;

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    String accountId = authentication.getName();
    String password = (String) authentication.getCredentials();

    UserDetails detail = userDetailsService.loadUserByUsername(accountId);
    if (!passwordEncoder.matches(password, detail.getPassword())) {
      throw new BadCredentialsException("비밀번호가 틀립니다.");
    }
    UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(accountId, password);
    return authenticationToken;
  }

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

이렇게 하면 UserDetail을 직접 가져와 passwordEncoder로 인코딩한 값과 비교하여 반환하는게 가능하다.

또한 인증이 성공했을 때 토큰 값을 시큐리티 컨텍스트에 제공해야 하므로 UsernamePasswordAuthenticationToken을 만들어 반환하도록 했다.