본문 바로가기

Spring/Spring Securitiy

8. Spring AuthenticationProvider 개요

Spring Security에서 AuthenticationProvider는 사용자 인증을 처리하는 핵심 인터페이스 중 하나입니다. 사용자의 자격 증명을 검사하고 인증이 성공하면 Authentication 객체를 반환합니다.

즉, 사용자의 username/password를 확인하거나, OAuth, JWT, API Key 등의 인증 방식을 처리하는 역할을 수행합니다.

 

1. AuthenticationProvider의 역할

  • Authentication 객체를 입력받아 인증을 수행.
  • 인증이 성공하면 Authenticated 상태의 Authentication 객체를 반환.
  • 인증 실패 시 예외(BadCredentialsException 등)를 발생.
  • 특정한 타입의 인증 객체만 처리할 수 있도록 제어 가능.

 

2. AuthenticationProvider 동작 흐름

Spring Security의 인증 흐름에서 AuthenticationProvider가 어떻게 동작하는지 자세히 살펴보겠습니다.

(1) 사용자가 로그인 요청을 보냄

사용자가 /login 엔드포인트를 통해 username과 password를 전송합니다.

(2) AuthenticationManager가 요청을 받음

AuthenticationManager는 AuthenticationProvider를 사용하여 인증을 수행합니다.

(3) AuthenticationProvider가 인증을 수행

  • supports() 메서드로 입력된 Authentication 타입을 확인.
  • authenticate() 메서드에서 인증 절차 수행.

(4) 인증이 성공하면 인증된 Authentication 객체를 반환

  • 사용자 정보를 포함하는 UsernamePasswordAuthenticationToken 객체를 반환.
  • 이후 SecurityContextHolder에 저장되어 요청이 인증된 상태가 됨.

(5) 인증이 실패하면 예외 발생

  • 예를 들어, 비밀번호가 틀리면 BadCredentialsException 발생.

 

3. AuthenticationProvider 구현 예제

커스텀 인증 제공자를 만들어 Spring Security에서 사용해 보겠습니다.

(1) CustomAuthenticationProvider 구현

package com.example.security.auth;

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.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.Collections;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final CustomUserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public CustomAuthenticationProvider(CustomUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.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())) {
            throw new BadCredentialsException("Invalid credentials");
        }

        // 인증 성공 시 Authentication 객체 반환
        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

(2) UserDetailsService 구현

package com.example.security.auth;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final Map<String, String> users = new HashMap<>();

    public CustomUserDetailsService() {
        // 임시 사용자 정보 (실제 환경에서는 DB 사용)
        users.put("user", "$2a$10$somethinghashedpassword"); // BCrypt 암호화된 비밀번호
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!users.containsKey(username)) {
            throw new UsernameNotFoundException("User not found");
        }
        return User.withUsername(username)
                .password(users.get(username))
                .roles("USER")
                .build();
    }
}

 

(3) SecurityConfig 설정

package com.example.security.config;

import com.example.security.auth.CustomAuthenticationProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;

@Configuration
public class SecurityConfig {

    private final CustomAuthenticationProvider authenticationProvider;

    public SecurityConfig(CustomAuthenticationProvider authenticationProvider) {
        this.authenticationProvider = authenticationProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(List.of(authenticationProvider));
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

4. 전체적인 동작 흐름 요약

  1. 사용자가 /login 요청을 보내 username/password를 전송.
  2. AuthenticationManager가 CustomAuthenticationProvider를 호출.
  3. CustomAuthenticationProvider가 UserDetailsService에서 사용자 정보를 조회.
  4. 비밀번호를 검증하고 인증 성공 시 Authentication 객체 반환.
  5. SecurityContextHolder에 인증 정보를 저장하여 인증된 요청으로 처리.

 

5. AuthenticationProvider 커스텀 활용 예시

  • JWT 인증: DB 대신 JWT 토큰을 기반으로 인증 처리.
  • OAuth2 인증: Google, Facebook 등의 OAuth2 기반 인증.
  • API Key 인증: API Key를 헤더로 전달받아 인증.

 

6. 결론

Spring Security의 AuthenticationProvider는 인증 로직을 직접 구현할 수 있는 강력한 확장 포인트입니다. 기본 DaoAuthenticationProvider를 사용할 수도 있지만, 특정 요구 사항에 맞춰 커스텀 AuthenticationProvider를 구현하면 더욱 유연한 인증 방식을 적용할 수 있습니다.