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 구현
과정은 다음과 같음:
- AuthenticationProvider 계약을 구현하는 클래스를 선언
- 커스텀 AuthenticationProvider 가 어떤 종류의 Authentication 객체를 지원할지 결정
- authenticate(), support() 를 재정의하여 구현함
- 커스텀 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 을 던짐
전체적인 동작 순서
- 클라이언트가 인증 요청을 보낸다
- SecurityFilterChain이 요청을 받아 CustomAuthenticationProvider의 supports() 메서드를 호출한다.
- supports() 메서드가 true를 반환하면 authenticate() 메서드가 호출된다.
- authenticate() 메서드 내에서:
- UserDetailsService의 loadUserByUsername() 메서드를 호출하여 사용자 정보를 가져온다.
- PasswordEncoder의 matches() 메서드를 호출하여 비밀번호를 검증한다.
- 인증이 성공하면 새로운 Authentication 객체를 생성하여 반환한다.
- 인증이 실패하면 BadCredentialsException을 던진다.
- SecurityFilterChain이 인증 결과를 클라이언트에게 반환한다.
이 과정을 통해 커스텀 AuthenticationProvider가 구현되고 인증 흐름이 이루어진다.
'Course > Spring Security' 카테고리의 다른 글
[netplix-security-a] AOP를 활용하여 비밀번호 암호화하기 (1) | 2024.10.03 |
---|---|
[netplix-security-a] PasswordEncoder 구현 (0) | 2024.10.03 |
[netplix-security-a] UserDetailsService 와 UserDetailsManager 구현 (1) | 2024.09.29 |
[netplix-security-a] UserDetails와 영속성 엔티티의 분리된 구현 (0) | 2024.09.29 |
[netplix-security-a] 기본 구성 재정의 (0) | 2024.09.29 |
댓글