본문 바로가기
개발 이론/Spring Security

[Spring Security] AuthenticationProvider(인증 공급자), SecurityContext

by dal_been 2023. 11. 7.
728x90

 

AuthenticationProvider는 인증논리를 담당 

→ 요청을 허용할지 말지 결정하는 조건과 명령 발견

AuthenticationManager는 HTTP 필터 계층에서 요청을 수신하고, 이 책임을 AuthenticationProdivder에 위임하는 구성요소

 

 

AuthenticationProvider

인증과 관련하여 메세지, 코드 등과 같이 다양한 인증논리를 이용한다.

이를 구현할 수 있는 것이 AuthenticationProvider 이다

 

인증 프로세스 중 요청 나타내기

 

Authentication 인터페이스

  • 인증 요청 이벤트를 나타내며
  • 애플리케이션에 접근을 요청한 엔티티의 세부정보를 담는다
  • 인증 요청 이벤트와 관련한 정보를 인증 프로세스 도중과 이후에 이용가능하다

애플리케이션 접근을 요청하는 사용자 = Principal(주체)

 

Authentication 은 Principal를 상속한다. 암호 같은 요구사항, 인증 요청에 대한 세부 정보를 더 추가할 수 있다

→ 계약의 주체를 나타냄

→ 또한 인증 프로세스 완료 여부, 권한 의 컬렉션 같은 정보를 추가로 가진다

 

public interface Authentication extends Principal, Serializable{
    Collection<? extends GrantedAuthority> getAuthorities(); //인증된 요청에 허가된 권한의 컬렉션 반환
    Object getCredentials();    // 인증 프로세스에 이용된 암호나 비밀 반환
    Object getDetails();   
    Object getPrincipal();
    boolean isAuthenticated();  // 인증 프로세스가 끝나면 true 반환, 진행중이면 false
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
    
}

 

 

 

맞춤형 인증 논리 구현

 

AuthenticationProvider인터페이스는

시스템의 사용자를 찾는 책임을 UserDetailService에 위임하고, PasswordEncoder로 인증 프로세스에서 암호를 관리한다

 

public interface AuthenticationProvider{
	
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    
    boolean supports(Class<?> authentication);
}

 

AuthenticationProvider는 Authentication과 강력하게 결합되어있다

Authentication 객체를 매개변스로 받고 Authentication객체를 반환한다.

 

1. authenticate(Authentication authentication)

  • 인증에 실패하면 exception을 투척
  • 메서드가 현재 AuthenticationProvider 구현에서 지원하지 않는 인증 객체를 받으면 null를 반환
  • 메서드는 완전히 인증된 객체면 Authentication인스턴스를 반환 -> isAuthenticated()메서드는 true를 반환
  • 반환된 객체에는 인증된 엔티티의 모든 필수 세부 정보가 포함됨
  • 다만 애플리케이션은 인스턴스에서 암호와 같은 민감한 데이터를 제거해야함

 

2.supports 메서드

  • 현재 AuthenticationProvider가 Authentication객체로 제공된 형식을 지원하면 true
  • 다만 true를 반환해도 authenticate메서드에서 null반환할 수 있음(요청 거부)

 

정리하자면  supperts 메서드는 어떤 잠금에 있어 유효한 카드인지 확인하고 

" 이카드 유효해" 하면 authenticate인증을 시작하는 것이다

 

 

맞춤형 인증 논리 적용

 

AuthenticationProvider를 구현하여 어떤 종류의 Authentication 객체를 지원할지 결정

이후 스프링 시큐리티에 새 AuthenticationProvider를 등록

 

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{
	
    @Autowired
    private UserDetailsService userDetailService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public boolean supports(Class<?> authenticationType){
    	return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    }
    
    @Override
    public Authentication authenticate(Authentication authentication){
    	
        String username=authentication.getName();
        String password=authentication.getCredentials.toString();
        
        UserDetails u=userDetailsService.loadUserByUserName(username);
        
        if(passwordEncoder.matches(password,u.getPassword()){
        	return new UsernamePasswordAuthenticationToken(
            	username, password, u.getAuthorities()
            );
        }else{
        	throw new BadCredentialsException("잘못됨");
        }
    }
    
}

 

만약 암호가 일치한다면 AuthenticationProvider는 요청의 세부정보를 포함하는 Authentication을 '인증됨'으로 표시하고 반환

 

 

이후 새 AuthenticationProvider를 WbSecurityConfig에 재정의 하면 된다

@Configuration
public class SecurityConfig {

		....

	@Bean
    public AuthenticationProvider customAuthenticationProvider() {
        CustomAuthenticationProvider provider = new CustomAuthenticationProvider();
        ...
        return provider;
    }
    
     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider());
    }

}

 

스프링 시큐리티 버전이 업그레이드 되면서 원래는 Adapter라는 것을 오버라이드 하여 cofigure()메서드에 등록하면 되는데 

최신버전에서는 찾아본 결과 @Bean으로 등록해야한다고 한다

또한 configure메서드에서 auth.authenticationProvider(customAuthenticationProvider())을 호출하여AuthenticationProvider을 등록해야함

 

 

 


SecurityContext

 

 

인증 프로세스가  끝난후 인증된 엔티티에 대한 세부 정보가 필요할 가능성이 크다

예를 들어 인증된 사용자의 이름이나 권한을 참조해야할 수 있다

인증 프로세스가 완료된 후에도 이 정보에 접근할 수 있을끼??

→ Authentication 객체를 저장하는 보안 컨텍스트(Security Context)를 통해 가능하다

 

 

예를 들어보자

"/products"라는 URI에 접속한다면 인증필터가 요청을 가로채고 인증관리자가 인증 책임을 다한다

만약 인증이 되면 세부정보가 보안 컨텍스트에 저장된다

그럼 해당 컨트롤러가 이제 보안컨텍스트에 있는 세부 정보를 이용할 수 있다

 

 

public interface SecurityContext extends Serializable{
	
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
    
}

 

→ SecurityContext 주책임 : Authentication 객체를 저장

 

그렇다면 SecuriyContext 자체는 어떻게 관리될까?? 세가지 전략을 제공하는 SecurityContextHolder라는 객체가 있다

1. MODE_THREADLOCAL 
- 각 스레드가 보안 컨텍스트에 각자의 세부 정보를 저장할 수 잇게 해준다
- 요청당 스레드 방식의 웹 애프리케이션에서는 각요청이 개별 스레드를 가지므로 이는 일반적인 접근법이다

2. MODE_INHERITABLETHREADLOCAL
- 위의 방식과 비슷하지만 비동기 메서드의 경우 보안 컨텍스트를 다음 스레드로 복사하도록 스프링 시큐리티에 지시한다
- 이 방식으로 @Async 메서드를 실행하는 새 스레드가 보안 컨텍스트를 상속하게 할 수 있다
- 비동기 메서드 : 작업을 시작한후 다른 작업을 수행할 수 있으며,  작업이 완료되면 해당 작업의 결과를 처리가능

3. MODE_GLOBAL
- 애플리케이션의 모든 스레드가 같은 보안 컨텟스트 인스턴스를 보게한다

 

 

 

보안 컨텍스트를 위한 보유 전략 이용

 

MODE_THREADLOCAL

  • 스프링 시큐리티의 기본전략
  • 각 스레드가 자신의 보안컨텍스트에만 접근가능하며 다른 스레드에는 접근 불가능하다
  • 다만 한 요청에서 예를들어 비동기 메서드 호출로 새 스레드가 호출되면 해당 새로운 스레드도 자체 보안 컨텍스트를 가져 상위 스레드의 세부 정보가 새 스레드의 보안 컨텍스트로 복사되지 않는다

 

만약 애플리케이션 엔드포인트 중 하나에서 보안 컨텍스트를 얻는 방법은

 

@GetMapping("/test")
public String test(){
    SecurityContext context=SecurityContextHolder.getContext();
    Authentication a= context.getAuthentication();
    
    return a.getName();
}

 

스프링은 인증을 메서드 매개변수로 곧바로 주입할 수 있어서 매개변수에 "Authenctication a"를 넣어도 가능하다

 

curl -u username:password http://localhost:8080/test

 

 

 

비동기 호출을 위한 보유 전략 이용

 

MODE_INHERITABLETHREADLOCAL

요청당 여러 스레드가 사용될때

→ 엔드포인트가 비동기되면 메서드를 시행하는 스레드와 요청을 수행하는 스레드가 다른 스레드가 됨

 

    @GetMapping("/bye")
    @Async
    public void goodbye() {
        SecurityContext context = SecurityContextHolder.getContext();
        String username = context.getAuthentication().getName();
    }
@Configuration
@EnableAsync
public class ProjectConfig {

    @Bean
    public InitializingBean initializingBean() {
        return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }
}

 

만약 @Bean 설정이 없다고 가정하고 실행해보면 

NullPointerException이 발생한다

그 이유는 메서드가 보안 컨텍스르를 상속하지 않는 다른 스레드에서 실행되기 때문이다.

따라서 Authorization객체는 null이 된다

 

이런 문제를 MODE_INHERITABLETHREADLOCAL 을 통해 해결가능하다

원래 스레드의 세부정보를 비동기 메서드의 새로 생성된 스레드로 복사한다

 

이 전략을 사용하고 싶다면 위의 처럼 @Bean으로 등록하거나 spring.security.strategy시스템 속성을 이용하면 된다

 

 

 

독립형 애플리케이션을 위한 보유 전략 이용

 

MODE_GLOBAL

 

모든 스레드가 같은 보안컨텍스트에 접근하는 것이다. 

모든 스레드가 해당 정보를 변경할 수 있기에 경합 상황이 일어날 수 잇으므로 동기화 처리를 해야한다

또한 SecurityContext는 스레드 안전을 지원하지 않으므로 이 전략에서는 모든 스레드가 SecurityContext객체에 접근할 수 있으므로 동시접근을 해결해야한다

 

 

위의 전략과 비슷하게 @Bean으로 등록하거나 시스템 속성을 이용하면 된다

 

 


HTTP Basic 인증과 양식 기반 로그인 인증 이해하기

 

 HTTP Basic 이용 및 구성

 

API와 같이 클라이언트가 직접 HTTP 요청을 보내는 경우 유용

 

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic(c -> {
            c.realmName("OTHER");
            c.authenticationEntryPoint(new CustomEntryPoint());
        });
        http.authorizeRequests().anyRequest().authenticated();
    }
}

 

  • realm 이란 보호되는 자원 또는 보호되는 영역을 나타내며 사용자가 해당 영역에 접근하기 위해 제공해야하는 자격증명(인증 정보)를 나타냄
  • realmName은 일반적으로 보안 구성에서 사용자에게 어떤 자격 증명을 제공해야 하는지에대한 안내메세지 또는 영역이름을 지정하는데 사용

 

그래서 만약 클라이언트가 서버에 HTTP를 요청하면 (authencticaionEntryPoint가 없다면)

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="OTHER"

 

와 같이 응답이 나올거이다

 

 

 

만약 인증 실패에 대한 응답을 맞춤구성하려면 AuthenticationEntryPoint를 구현하면 된다

public class CustomEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
        httpServletResponse.addHeader("응답을 구성하자");
        httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value());
    }
}

 

이렇게 하면 헤더에 "응답을 구성하자"라는 내용이 추가될 것이다

 

 

 

양식 기반 로그인으로 인증 구현

 

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomAuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private CustomAuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .successHandler(authenticationSuccessHandler)
            .failureHandler(authenticationFailureHandler)
        .and()
            .httpBasic();

        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

 

만약 formLogin()을 이용하면 스프링 시큐리티에서 제공하는 간단한 로그인, 로그아웃 양식을 제공하여 이용가능하다

위의 같이 Handler말고 " .defaultSuccesUrl("/home",true) " 을 통해 URI호출이 가능하다.

다만 더 세부적인 맞춤 구성이 필요하다면 hanlder를 이요하면 된다

 

 

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
        var authorities = authentication.getAuthorities();

        var auth = authorities.stream()
                    .filter(a -> a.getAuthority().equals("read"))
                    .findFirst();

        if (auth.isPresent()) {
            httpServletResponse.sendRedirect("/home");
        } else {
            httpServletResponse.sendRedirect("/error");
        }
    }
}



@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)  {
        httpServletResponse.setHeader("failed", LocalDateTime.now().toString());
    }
}

 

 


정리

  • AuthenticationProvider 구성 요소를 이용하면 맞춤형 인증 논리를 구현할 수 있다
  • 맞춤형 인증논리를 구현할때 책임을 분리하는 것이 좋다→ AuthenticationProvider는 사용자 관리는 UserDetailsService에 위임하고 암호 검증책임은 PasswordEncoder에 위임한다
  • SecurityContext는 인증이 성공한 후 인증된 엔티티에 대한 세부 정보를 유지한다
  • 보안 컨텍스트를 관리하는 전략들은 다른 스레드에서 보안 컨텍스트 세부 정보에 대한 접근 방법들이 다르다
  • formLogin 인증 메서드는 세부적으로 맞춤구성이 가능하며 HTTP basic방식과 함께 이용해 두 인증 유형을 모두 지원할 수 도 있다