본문 바로가기

SPRING/SpringSecurity

[스프링 시큐리티 완전 정복| 실전 프로젝트 | 회원 인증 시스템] 커스텀 UserDetailService, AuthenticationProvider 구현하기

 

 

UserDetailsService

 

 



사용자 인증을 처리하는 핵심 인터페이스 중 하나로, 사용자의 정보를 불러와 인증을 위한 로직을 처리하는 역할을 한다. 주로 사용자 데이터를 데이터베이스에서 조회하고, 인증 객체로 반환하는 데 사용된다.

UserDetailsService의 주요 기능:

  • 사용자 조회: UserDetailsService는 사용자의 이름(주로 사용자 ID, username)을 입력받아 해당 사용자의 세부 정보를 불러오는 책임을 가진다.
  • 인증 처리: 불러온 사용자 정보는 Spring Security에 의해 인증 절차에 사용된다.
  • UserDetails 반환: 사용자의 세부 정보는 UserDetails라는 인터페이스로 반환된다. 이 객체는 인증에 필요한 정보를 담고 있다(사용자 이름, 비밀번호, 권한 등).
 

 

 

 


UserDetails 인터페이스

 

 



loadUserByUsername 메서드가 반환하는 UserDetails 객체는 다음과 같은 사용자 정보를 포함한다:

  • getUsername(): 사용자의 이름을 반환한다.
  • getPassword(): 사용자의 암호화된 비밀번호를 반환한다.
  • getAuthorities(): 사용자가 가진 권한(역할)을 반환한다.
  • isAccountNonExpired(): 계정이 만료되지 않았는지 여부를 반환한다.
  • isAccountNonLocked(): 계정이 잠기지 않았는지 여부를 반환한다.
  • isCredentialsNonExpired(): 사용자 자격 증명이 만료되지 않았는지 여부를 반환한다.
  • isEnabled(): 계정이 활성화 상태인지 여부를 반환한다.
 

 

 


코드 설명

 

@Service("userDetailsService")  // Spring Bean으로 등록, "userDetailsService" 이름으로 스프링 컨텍스트에서 참조 가능
@RequiredArgsConstructor  // final로 선언된 필드의 생성자를 자동으로 생성해주는 Lombok 어노테이션
public class FormUserDetailsService implements UserDetailsService {  // Spring Security의 UserDetailsService 인터페이스를 구현하여 사용자 인증 처리

    private final AccountRepository accountRepository;  // 사용자 정보를 데이터베이스에서 조회하는 AccountRepository 의존성 주입

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  // username으로 사용자를 조회하고 UserDetails 반환

        Account account = accountRepository.findByUserName(username);  // 데이터베이스에서 사용자를 조회 (AccountRepository 사용)
        if(account == null){  // 사용자가 데이터베이스에 없을 경우 예외 처리
            throw new UsernameNotFoundException("No user found with username" + username);  // 사용자 이름으로 사용자를 찾을 수 없으면 예외 발생
        }

        // Account 엔티티에서 정보를 가져와 AccountDto 객체를 생성
        AccountDto accountDto = AccountDto.builder()  
                .id(account.getId())  // 사용자의 ID 설정
                .userName(account.getUserName())  // 사용자의 이름 설정
                .password(account.getPassword())  // 사용자의 암호화된 비밀번호 설정
                .roles(account.getRoles())  // 사용자의 역할 (권한) 설정
                .build();

        // 사용자 권한(roles)을 SimpleGrantedAuthority 객체로 변환하여 리스트로 생성
        List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(account.getRoles()));

        // AccountContext를 UserDetails로 반환. AccountContext는 UserDetails 인터페이스를 구현한 사용자 세부 정보
        return new AccountContext(accountDto, authorities);
    }
}

 

 

 

 

 

AuthenticationProvider

 



1. 폼 기반 인증 흐름

Spring Security에서의 폼 기반 인증은 보통 다음과 같은 과정을 거친다:

  1. 사용자가 로그인 페이지에서 사용자 이름과 비밀번호를 입력한다.
  2. 서버로 폼 데이터가 제출되면, **UsernamePasswordAuthenticationFilter*가 요청을 가로채서 인증 시도를 한다.
  3. UsernamePasswordAuthenticationFilter는 사용자가 입력한 username과 password를 **AuthenticationManager*에 전달한다.
  4. AuthenticationManager는 등록된 여러 AuthenticationProvider 중에서 적절한 제공자에게 인증을 위임한다.
  5. 일반적으로 **DaoAuthenticationProvider*가 폼 인증을 처리하고, 이를 통해 사용자 정보와 비밀번호를 검증한다.

2. DaoAuthenticationProvider의 역할

DaoAuthenticationProvider는 폼 인증에서 실제로 사용자의 비밀번호와 사용자 정보를 검증하는 AuthenticationProvider의 구현체다. 주로 UserDetailsService 비밀번호 인코더를 사용하여 인증을 처리한다.

DaoAuthenticationProvider의 주요 구성 요소:

  1. UserDetailsService:
    • UserDetailsService는 사용자의 이름(주로 username)을 입력받아, 사용자의 세부 정보를 조회하는 인터페이스다.
    • 이 인터페이스는 사용자 정보를 데이터베이스에서 조회하거나 외부 API로부터 불러오는 역할을 한다.
  2. PasswordEncoder:
    • PasswordEncoder는 저장된 비밀번호(해시된 값)와 사용자가 입력한 비밀번호를 비교하는 기능을 제공한다.
    • 일반적으로 BCryptPasswordEncoder와 같은 해시 알고리즘을 사용해 비밀번호를 인코딩하고 검증한다.

3. 폼 기반 인증 처리 과정

DaoAuthenticationProvider가 폼 인증을 처리하는 기본적인 과정은 다음과 같다:

  1. 사용자가 입력한 username을 통해 UserDetailsService를 사용하여 데이터베이스에서 사용자 정보를 조회한다.
  2. 조회된 사용자 정보에서 비밀번호를 가져온다.
  3. 사용자가 입력한 비밀번호와 저장된 비밀번호를 **PasswordEncoder*로 비교한다.
  4. 비밀번호가 일치하면 인증이 성공하고, Authentication 객체가 반환된다.
  5. 인증이 성공하면 세션 또는 JWT 토큰을 생성하여 사용자에게 부여한다.
 

 


코드 설명

 

@Component("authenticationProvider")  // 이 클래스를 Spring의 Bean으로 등록, "authenticationProvider" 이름으로 사용 가능
@RequiredArgsConstructor  // Lombok 어노테이션으로, final로 선언된 필드의 생성자를 자동으로 생성
public class FormAuthenticationProvider implements AuthenticationProvider {  // Spring Security의 AuthenticationProvider 인터페이스를 구현하여 인증 처리

    private final UserDetailsService userDetailsService;  // 사용자 정보를 로드하는 UserDetailsService 의존성 주입
    private final PasswordEncoder passwordEncoder;  // 비밀번호 검증을 위한 PasswordEncoder 의존성 주입

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        // 사용자가 입력한 로그인 ID를 가져옴
        String loginId = authentication.getName();

        // 사용자가 입력한 비밀번호를 가져옴
        String password = (String) authentication.getCredentials();

        // 입력된 loginId를 통해 사용자 정보를 로드 (AccountContext를 통해 사용자 정보와 권한 정보 포함)
        AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(loginId);

        // 입력된 비밀번호와 저장된 비밀번호를 PasswordEncoder로 비교 (일치하지 않으면 예외 발생)
        if (passwordEncoder.matches(password, accountContext.getPassword())) {
            throw new BadCredentialsException("Invalid password");  // 비밀번호가 틀리면 인증 실패 예외 발생
        }

        // 인증 성공 시 UsernamePasswordAuthenticationToken 객체를 생성하여 반환
        // 여기서 accountContext의 DTO와 권한 정보를 포함하여 인증 토큰 생성
        return new UsernamePasswordAuthenticationToken(accountContext.getAccountDto(), null, accountContext.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 이 AuthenticationProvider가 지원하는 토큰의 타입을 명시 (UsernamePasswordAuthenticationToken 타입을 지원)
        return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
    }
}