본문 바로가기
Course/Spring Security

[netplix-security-a] AuthenticationProvider 구현

by Lpromotion 2024. 10. 3.
Netplix 구독형 멤버십 프로젝트로 배우는 SpringSecurity
[Ch 4. AuthenticationProvider와 인] - 01. AuthenticationProvider 살펴보기
강의를 바탕으로 실습 내용을 정리하였습니다.

목차
 

1. 인증 구현

인증 논리를 담당하는 AuthenticationProvider 를 알아보자

  • 요청을 허용할 것인지 정할 수 있음

 

AuthenticationManager 는 HTTP 필터 계층에서 요청을 수신하고 이 책임을 AuthenticationProvider 에게 위임함

 

두 케이스로 정리할 수 있음

  • 사용자를 찾을 수 없음: 애플리케이션이 사용자를 인식하지 못해 권한 부여 프로세스에 위임하지 않고 요청을 거부함
  • 사용자를 찾을 수 있음: 사용자 정보가 저장되어 있기 때문에 애플리케이션이 이를 활용해 권한 부여를 할 수 있음

 

2. AuthenticationProvider 와 SecurityContext

스프링 시큐리티의 인증 흐름에서 AuthenticationProvider 와 SecurityContext 에 대해 알아보고자 함

 

AuthenticationProvider 란?

AuthenticationProvider 는 맞춤형 인증 논리를 정의할 수 있음

예를 들어, 단순 비밀번호 기반의 인증 뿐만 아니라 지문, SMS 코드 등 다양한 방법으로 신원 증명을 할 수 있음

  • 어떠한 시나리오가 필요하더라도 이를 구현할 수 있도록 지원하는 것이 프레임워크의 목적임

 

3. Authentication 과 Principal

Authentication: 인증이라는 의미를 가지고 있음

  • 스프링 시큐리티에서의 Authentication 은 인증 프로세스의 필수 인터페이스
  • 인증 요청 이벤트를 나타냄
  • 애플리케이션에 접근을 요청한 엔티티의 세부 정보를 담음

 

애플리케이션에 접근을 요청하는 사용자를 Principal 이라고 함

  • “주체” 라고 함

 

두 인터페이스를 한번 살펴보자

 

Authentication 인터페이스는 Principal 인터페이스를 확장함

 

Principal 메소드 소개

getName(): 인증하려는 사용자는 이름이 필요함 (아이디)

 

Authentication 메소드 소개

Authentication 은 Principal 만 포함하는 것이 아니라 인증 프로세스의 완료 여부, 권한의 컬렉션 정보를 추가로 포함하고 있음

  • getAuthorities(): 인증 후 사용자의 이용 권리와 권한. 컬렉션으로 반환함
  • getCredential(): 스프링 시큐리티에서의 인증은 사용자는 암호가 있어야 함 (비밀번호, 지문 등)
  • getDetails(): 사용자 요청에 대한 추가 세부 정보
  • isAuthenticated(): 사용자가 인증되었는지를 나타냄. 인증 프로세스가 끝났으면 true 를 반환하고 아직 진행 중이면 false 를 반환함

 

4. AuthenticationProvider 기본 구현

AuthenticationProvider 인터페이스의 기본 구현은

  • 사용자를 찾는 UserDetailsService 에 위임함
  • PasswordEncoder 로 인증 프로세스에서 암호를 관리함
  • Authentication 인터페이스와 강결합이 되어 있음

 

authenticate() 와 support()

 

authenticate(): Authentication 객체를 파라미터로 받고 다시 Authentication 을 반환함

  • 인증에 실패하면 AuthenticationException 을 던짐
  • AuthenticationProvider 구현체에서 지원되지 않는 인증 객체를 받으면 null 을 반환함
  • 반환되는 Authentication 객체에는 인증된 사용자의 필수 세부 정보가 포함됨

 

supports(): 현재 AuthenticationProvider 가 Authentication 객체로 제공된 형식을 지원하면 true 를 반환하도록 구현함

  • 객체에 대해 true 를 반환하더라도 authenticate() 메소드가 null 을 반환하면 요청을 거부할 수 있음
  • 여러 AuthenticationProvider 가 설정된 경우, suport() 메서드를 통해 적절한 provider를 선택함

 

5. AuthenticationProvider 와 AuthenticationManager

인증 요청을 허용하거나 거부하기 위해 AuthenticationManager 와 AuthenticationProvider 는 서로 연결되어 있음

  • AuthenticationManager 가 중앙에서 사용자를 허용할지 거부할지 판단하는 컨트롤 타워
  • AuthenticationManager 는 AuthenticationProvider 에게 인증 작업을 위임하여 이를 판단함

 

AuthenticationManager 는 사용 가능한 인증 공급자 중 하나에 인증을 위임함

  • AuthenticationProvider 는 주어진 인증 유형을 지원하지 않거나, 객체 유형은 지원하지만 해당 특정 객체를 인증하는 방법을 모를 수 있음
  • 인증을 평가한 후 요청이 올바른지 판단할 수 있는 AuthenticationProvider 가 AuthenticationManager 에 응답함

 

예를 들어, 열쇠와 카드로 잠금 장치를 여는 시스템이 있다고 하자 (열쇠와 카드는 인증 공급자)

  • 열쇠 담당 인증 공급자는 카드 인증에 대해서는 처리할 수는 없지만 카드 인증 공급자는 카드 인증에 대해 처리할 수 있음
  • 이 역할을 하기 위해 support 가 필요함

 

카드 인증 공급자는 카드 인증 요청에 대해 supports()는 true를 반환함. 그러니 실제 인증 과정에서 유효하지 않은 카드인 경우 authenticate() 메서드는 예외 또는 null을 반환할 수 있음

 

6. 커스텀 AuthenticationProvider 구현

과정은 다음과 같음:

  1. AuthenticationProvider 계약을 구현하는 클래스를 선언
  2. 커스텀 AuthenticationProvider 가 어떤 종류의 Authentication 객체를 지원할지 결정
    • authenticate(), support() 를 재정의하여 구현함
  3. 커스텀 AuthenticationProvider 구현의 인스턴스를 스프링 시큐리티에 등록

 

build.gradle 의존성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

AuthenticationProvider 를 상속받아 구현하는 CustomAuthenticationProvider 클래스를 생성함

  • @Component 어노테이션으로 스프링에 등록
  • CustomAuthenticationProvider 는 UsernamePasswordAuthenticationToken 을 지원함
  • UsernamePasswordAuthenticationToken 은 사용자 아이디와 암호를 이용하는 표준 인증 요청을 나타냄
package fast.campus.fcss01.authentication;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component // 스프링에 등록
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            return null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // UsernamePasswordAuthenticationToken: 사용자의 아이디와 암호를 이용하는 표준 인증 요청
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

 

AuthenticationProvider 의 authenticate() 를 구현하기에 앞서 설정 클래스(SecurityConfig) 생성

  • @Configuration 어노테이션으로 설정 클래스임을 나타냄
  • PasswordEncoder 로는 NoOpPasswordEncoder 를 등록
  • UserDetailsService 로는 InMemoryUserDetailsManager 를 등록
package fast.campus.fcss01.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration // 설정 클래스임을 나타냄
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager();
    }
}

 

Bean 으로 등록한 UserDetailsService 와 PasswordEncoder 를 의존성 주입

조회한 사용자의 비밀번호와 입력받은 비밀번호를 passwordEncoder 로 비교

만약 matches() 에서 false 가 반환되면 BadCredentialsException 이 던져짐

true 가 반환되면 Authentication 의 authenticated 를 true 로 설정하고 반환함

package fast.campus.fcss01.authentication;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component // 스프링에 등록
@RequiredArgsConstructor // 생성자 자동 생성
public class CustomAuthenticationProvider implements AuthenticationProvider {

    // Bean 으로 등록한 UserDetailsService 와 PasswordEncoder 를 의존성 주입
    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 user = userDetailsService.loadUserByUsername(username);

        // 조회한 사용자의 비밀번호와 입력받은 비밀번호를 passwordEncoder 로 비교
        if(passwordEncoder.matches(password, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(
                    username,
                    password,
                    user.getAuthorities()
            );
        }

        throw new BadCredentialsException("credential exception");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // UsernamePasswordAuthenticationToken: 사용자의 아이디와 암호를 이용하는 표준 인증 요청
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

SecurityFilterChain 에 대한 Bean 설정

  • CustomAuthenticationProvider 를 의존성 주입
  • HttpSecurity 를 파라미터로 받고 authenticationProvider 를 설정함
  • http.build() 를 통해서 리턴하면 Spring Security에 반영됨
package fast.campus.fcss01.config;

...

@Configuration // 설정 클래스임을 나타냄
@EnableAsync
public class SecurityConfig {

    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    ...

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // HttpSecurity 를 파라미터로 받고 authenticationProvider 를 설정함
        // CustomAuthenticationProvider 를 의존성 주입
        http.authenticationProvider(customAuthenticationProvider);
        return http.build();// 리턴하면 Spring Security에 반영됨
    }
}

 

7. AuthenticationProvider 에 의해 구현된 인증 흐름

AuthenticationProvider 는 인증 요청을 검증하기 위해 주어진 UserDetailsService 의 구현으로 사용자 세부 정보를 로드하고 PasswordEncoder 로 암호를 검증함

  • 사용자가 없거나 암호가 맞지 않으면 AuthenticationProvider 는 AuthenticationException 을 던짐

 

전체적인 동작 순서

  1. 클라이언트가 인증 요청을 보낸다
  2. SecurityFilterChain이 요청을 받아 CustomAuthenticationProvider의 supports() 메서드를 호출한다.
  3. supports() 메서드가 true를 반환하면 authenticate() 메서드가 호출된다.
  4. authenticate() 메서드 내에서:
    1. UserDetailsService의 loadUserByUsername() 메서드를 호출하여 사용자 정보를 가져온다.
    2. PasswordEncoder의 matches() 메서드를 호출하여 비밀번호를 검증한다.
  5. 인증이 성공하면 새로운 Authentication 객체를 생성하여 반환한다.
  6. 인증이 실패하면 BadCredentialsException을 던진다.
  7. SecurityFilterChain이 인증 결과를 클라이언트에게 반환한다.

이 과정을 통해 커스텀 AuthenticationProvider가 구현되고 인증 흐름이 이루어진다.

반응형

댓글