공부기록/Spring Security

[이론] 스프링 시큐리티2

jhs0129 2022. 12. 21. 12:14
320x100
반응형

목차

스프링 시큐리티 기본 동작 흐름도

이번에는 AuthenticationProvider, AuthenticationManger SecurityContext에 대해서 알아본다

AuthenticationProvider

Authentication: Principal 확장

인증 프로세스의 필수 인터페이스로 인증 요청 이벤트를 나타냄

요청한 엔티티의 세부정보를 담음

요청하는 사용자: Principal

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // 권한 반환
    Object getCredentials(); // 암호 반환
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated(); // 인증 종료 여부
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

AuthenticationProvider

  • 인증 논리 담당, 요청을 허용할지 결정하는 조건 명령 발견
  • AuthenticationManager 로부터 책임을 위임 받음
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    //인증 실패시 예외 투척
    //인증 성공시 Authentication 객체 반환 - isAuthenticated: true
    //인증 실패시 null 반환 - 형식은 Authentication 이지만 세부정보가 일치 하지 않음
    boolean supports(Class<?> authentication);
    //Authentication 객체로 제공된 형식이면 true
}

authenticate()는 UserDetailsService, PassworEncoder를 가지고 해당 유저가 존해하는지 여부를 판별하도록 재정의 하면된다

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

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

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (passwordEncoder.matches(password, userDetails.getPassword())) {
            return new UsernamePasswordAuthenticationToken(
                    username,
                    password,
                    userDetails.getAuthorities()
            );
        }
        throw new BadCredentialsException("Something went wrong");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //사용자 이름과 암호를 이용하여 인증 요청을 나타냄
        // Authentication 구현의 한 종류
        return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
    }
}

메소드 설명

  • supports - 집 문을 열 수 있는 방법이 열쇠, 카드, 비밀번호, 지문 인지 확인
  • authenticate - 집 문을 열 때 알맞은 정보로 열었는지 (ex) 지문이 인식이 되어있다, 비밀번호가 일치한다

AuthencicationManager

Filter로 부터 받은 요청을 Provider를 이용해 요청을 인증한다

재정의한 CustomAuthenticationProvider를 AuthencicationManager가 가질 수 있도록 설정을 해줘야한다

// 설정파일
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);

    builder.authenticationProvider(authenticationProvider);
    return builder.build();
}

SecurityContext

  • 인증 로직이 완료된 후 Authentication 객체를 저장 관리하는 역할
  • SecurityContext에 접근하여 사용자 정보를 사용할 수 있다
public interface SecurityContext extends Serializable{
  Authentication getAuthentication();
  void setAuthentication(Authentication authentication);
}

관리 전략

SecurityContextHolder 사용하여 관리자 역할

  • MODE_THREADLOCAL: 각 스레드가 저장 가능, 일반적인 방법
  • MODE_INHERITABLETHREADLOCAL: 비동기 메서드경우 스레드 복사하여 사용
  • MODE_GLOBAL: 모든 스레드가 같은 컨텍스트 사용

HttpBasic, FormLogin

HttpBasic

특정 resource에 대한 접근을 요청할때 브라우저가 사용자에게 username과 password를 확인해 인가를 제한하는 방법

전체 과정

  • Client는 Server에게 Resource를 요청한다.
  • Client가 요청한 Resource를 이용하기 위해서는 인증이 필요하다. 따라서 Server는 Client에게 WWW-Authenticate Header를 통해 인증 필요성을 Client에게 전달한다. Basic 문자열을 통해 Basic 인증 과정이 필요하다는걸 Client에게 알린다.
  • 인증 요청을 받은 Client는 ID:Password 문자열을 Base64로 Encoding한 String을 Authorize Header에 추가한뒤 다시 한번 Server에게 Resource를 요청한다.
  • Encoding된 ID:Passowrd 문자열을 받은 Server는 자신이 Encoding한 값과 일치하는지 확인한 후 일치하면, 요청한 Resource를 전달한다.

FormLogin

사용자가 로그인 하지 않았으면 로그인 양식으로 redirect된다

http.formLogin() 사용

  • defaultSuccessUrl 설정
  • AuthenticationSuccessHandler 구현 - .successHandler()
  • AuthenticationFailureHandler 구현 - .failureHandler()

Handler를 통해 추후 행동에 대해 명시할 수 있다

추가 코드 삽질내용

추가로 어떠한 과정으로 Spring Security에 AuthenticationManager, AuthenticationProvider, UserDetailsService 등록되는지 궁금하면 아래 코드를 확인해보세요

더보기

ProviderManager(AuthenticationManager) -> AuthenticationProvider -> UserDetailsService

ProviderManager생성시 Bean으로 등록된 UserDetailsService, PasswordEncoder를 가진 AuthenticationProvider 후 리스트로 가지고 있음

  • 내가 Bean 등록한 AuthenticationProvider 자동 등록
class InitializeAuthenticationProviderManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { 
    @Override 
    public void configure(AuthenticationManagerBuilder auth) { 
        if (auth.isConfigured()) { 
            return; 
        } 
        AuthenticationProvider authenticationProvider = 
            getBeanOrNull(AuthenticationProvider.class); 
        if (authenticationProvider == null) { 
            return; 
        } 
        auth.authenticationProvider(authenticationProvider); 
    } 
}

등록하는 과정에서 보면 AuthenticationProvider객체를 Bean 등록을 해두면 자동으로 사용할 수 있게 등록을 하는 것 같다

대신 AuthenticationProvider가 두개 이상일 경우 등록이 불가하다

 

  • DaoAuthenticationProvider default 등록
class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
        PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
        UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
        
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        provider.setUserDetailsPasswordService(passwordManager);
        
        provider.afterPropertiesSet();
        auth.authenticationProvider(provider);
    }
}

기본적으로 DaoAuthenticationProvider를 등록을 해주는데 이때 bean으로 등록 된 UserDetailsService와 PasswordEncoder를 찾아와 설정해준다

 

  • AuthenticationProvider리스트를 가진 ProviderManager 생성
public class AuthenticationManagerBuilder extends AbstractConfiguredSecurityBuilder<AuthenticationManager, AuthenticationManagerBuilder> implements ProviderManagerBuilder<AuthenticationManagerBuilder> { 
    private List<AuthenticationProvider> authenticationProviders = new ArrayList<>(); 

    @Override 
    public AuthenticationManagerBuilder authenticationProvider(AuthenticationProvider authenticationProvider) { 
        this.authenticationProviders.add(authenticationProvider); 
        return this; 
    }

    @Override 
    protected ProviderManager performBuild() throws Exception { 
        if (!isConfigured()) { 
            return null; 
        } 
        ProviderManager providerManager = new ProviderManager(this.authenticationProviders, this.parentAuthenticationManager); 
        return providerManager; 
    } 
}
  • ProviderManager에서 해당 Authentication을 담당하는 AuthenticationProvider 선택 후 인증 로직 수행
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { 
    @Override 
    public Authentication authenticate(Authentication authentication) throws AuthenticationException { 
        Class<? extends Authentication> toTest = authentication.getClass(); 
        AuthenticationException lastException = null; 
        Authentication result = null; 
        for (AuthenticationProvider provider : getProviders()) { 
            if (!provider.supports(toTest)) { 
                continue; 
            } 
            try { 
                result = provider.authenticate(authentication); // 검증 
            if (result != null) { 
                copyDetails(authentication, result); break; 
                } 
            } catch (Exception ex) { 
                lastException = ex; 
            } 
        } 
    } 
}
320x100
반응형