이전 포스팅에서 이어지는 내용이에요.
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService customUserDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails loadedUser = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(loadedUser, null, loadedUser.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
실질적인 인증 로직을 위해 AuthenticationProvider interface를 구현한 클래스가 필요한데요.
AuthenticationProvider를 implements 하게 되면 authenticate(), supports() 메서드를 구현하게 되요.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
}
두 개의 생성자를 통해 파악할 수 있는 것이 있어요. 첫번째 생성자에서 setAuthenticated(false)를 보면 인증전의 생성자를 의미하고, 두번째 생성자는 setAuthenticated(ture)를 보아 인증이 성공한 객체 생성자를 의미해요.
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("not found"));
}
}
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomAuthenticationProvider authProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
}
인증 로직이 구현된 CustomAuthenticationProvider를 ProviderManager가 알 수 있도록 ProviderManager에 등록해줘야 해요.
시큐리티 설정을 위한 SecurityConfig 클래스에서 위와 같이 authenticationProvider를 추가해주면 끝나요.
이렇게 구현된 인증 절차를 통해 인증이 완료되면 Authentication을 SecurityContextHolder 객체 안의 SecurityContext에 저장합니다.
Authentication authentication = SecutiryContextHolder.getContext().getAuthentication();
그리고 이러한 방식으로 저장된 인증 객체를 전역적으로 사용하면 되요.
SecurityContext 객체를 저장하고 감싸고 있는 wrapper 클래스로 SecurityContextHolder는 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 콘텍스트에 대한 세부 정보가 저장되는 곳이에요.
Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있습니다. ThreadLocal에 저장되어 아무 곳에서나 참조가 가능하도록 설계되어 있어요.
AuthenticationProvider 인터페이스에서 DB에 있는 사용자의 정보를 가져오려면, UserDetailsService 인터페이스를 사용한다.
UserDetailsService 인터페이스는 화면에서 입력한 사용자의 username으로 loadUserByUsername() 메소드를 호출하여 DB에 있는 사용자의 정보를 UserDetails 형태로 가져옴. 만약 사용자가 존재하지 않으면 예외를 던짐(UsernameNotFound). 이렇게 DB에서 가져온 이용자의 정보와 화면에서 입력한 로그인 정보를 비교하게 되고, 일치하면 Authentication 참조를 리턴하고, 일치 하지 않으면 예외를 던짐.
인증이 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행한다.(실패시 AuthenticationFailureHandler를 실행한)