[Spring Security] 사용자 관리

황승수 (Seungsu)·2023년 4월 1일
1

Spring Security

목록 보기
2/3

Spring Security 인증 흐름

  • 인증 필터(AuthenticationFilter)가 요청을 가로챈다.
  • 인증 책임이 인증 관리자(AuthenticationManager)에 위임된다.
  • 인증 관리자는 인증 논리를 구현하는 인증 공급자(AuthenticationProvider)를 이용한다.
  • 인증 공급자는 UserDetailsService 또는 UserDetailsManager로 사용자를 찾고, PasswordEncoder로 암호를 검증한다.
  • 인증 결과가 필터에 반환된다.
  • 인증된 엔티티에 관한 세부 정보가 SecurityContext에 저장된다.

위 아키텍쳐는 스프링 시큐리티가 구현하는 인증 프로세스의 근간으로, 모든 스프링 시큐리티 구현이 이에 의존한다.


사용자 관리 흐름


사용자 관리를 위해서는 UserDetailsService 또는 UserDetailsManager 인터페이스를 구현해야 한다.
사용자를 인증하는 기능만 필요한 경우 UserDetailsService 인터페이스만 구현하고, 사용자 추가, 수정, 삭제 작업 등의 추가적인 사용자 관리 기능이 필요하다면 UserDetailsManager 인터페이스를 구현한다.

위 아키텍쳐에 따른 각 구성 요소의 역할은 다음과 같다.

  • UserDetailsService: 사용자 이름으로 찾은 사용자 세부 정보를 반환
  • UserDetails: 사용자 기술
  • GrantedAuthority: 사용자의 권한을 나타냄
  • UserDetailsManager: UserDetailsService를 확장하여 암호 생성, 삭제, 변경 등의 작업을 추가

사용자 관리 아키텍쳐 구현하기

UserDetails

스프링 시큐리티가 사용자를 이해할 수 있으려면, 사용자 정의는 UserDetails 인터페이스를 준수해야 한다. UserDetails 인터페이스의 구조와, 선언된 메서드의 역할은 다음과 같다.

public interface UserDetails extends Serializable {
	String getUsername();
    String getPassword();
    Collection<? extends GrantedAuthority> getAuthorities();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}
  • getUsername(), getPassword() : 인증 과정에 사용되는 사용자 이름, 암호 반환
  • getAuthorities() : 사용자에게 부여된 권한 그룹을 반환
  • isAccountNonExpired() : 계정 만료 여부를 나타내는 메서드
  • isAccountNonLocked() : 계정 잠금 여부를 나타내는 메서드
  • isCredentialsNonExpired() : 자격 증명 만료 여부를 나타내는 메서드
  • isEnabled() : 계정 비활성화 여부를 나타내는 메서드

UserDetails 구현 방법

  • UserDetails 인터페이스 구현
  • 빌더를 이용한 인스턴스 생성

UserDetails 인터페이스 구현

public class SimpleUser implements UserDetails {
    private final String username;
    private final String password;

    public SimpleUser(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(() -> "READ");
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

또는 빌더를 이용해 UserDetails 형식 인스턴스 생성

만일 UserDetails 인터페이스의 맞춤형 구현이 필요 없다면 스프링 시큐리티에서 제공하는 빌더 클래스로 간단한 불변 사용자 인스턴스를 만들 수 있다.

UserDetails u = User.withUsername("seungsu")
				.password("1111")
                .authorities("read", "write")
                .accountExpired(false)
                .disabled(true)
                .build();

JPA 엔티티와 결합하기

엔티티가 직접 UserDetails를 구현하는 방식으로 설계하게 되면 두 책임이 한 클래스에 전가되는 문제가 있다. 따라서 User 엔티티와 SecurityUser 클래스로 책임을 나누어 User 엔티티가 여러 다른 작업을 구현하지 않도록 설계한다.

User 엔티티
User 클래스는 JPA 엔티티 책임만 담당한다.

@Entity
public class User {
    @Id
    private Long id;
    private String username;
    private String password;
    private String authority;

    @Builder
    public User(String username, String password, String authority) {
        this.username = username;
        this.password = password;
        this.authority = authority;
    }
    
    // ...
}

SecurityUser
SecurityUser 클래스는 사용자 세부 정보를 스프링 시큐리티가 이해할 수 있는 UserDetails 인터페이스에 매핑하는 작업만 담당한다.

public class SecurityUser implements UserDetails {
    private final User user;

    public SecurityUser(User user) {
        this.user = user;
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(user::getAuthority);
    }
    
    // ...
}

GrantedAuthority

사용자 세부 정보의 정의에 이용되며 사용자에게 허가된 이용 권리를 나타내는 인터페이스.
일반적으로 사용자는 하나 이상의 권한을 가진다.

@FunctionalInterface
public interface GrantedAuthority extends Serializable {
	String getAuthority();
}
  • getAuthority() : 권한의 이름을 String으로 반환

GrantedAuthority를 구현하는 두가지 방법

  • @FunctionalInterface 어노테이션을 지정하고 람다식으로 구현
  • SimpleGrantedAuthority 클래스로 GrantedAuthority 형식의 불변 인스턴스 생성
GrantedAuthority g1 = () -> "READ";
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");

UserDetailsService & UserDetailsManager

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

인증 구현은 loadUserByUsername 메서드를 호출해 주어진 사용자 이름을 가진 사용자 세부 정보를 얻는다. 이 메서드가 반환하는 사용자는 UserDetails 인터페이스의 구현체로, 사용자를 찾을 수 없는 경우 UsernameNotFoundException 런타임 예외가 발생한다.

즉 인증 공급자(AuthenticationProvider)는 인증 논리에서 UserDetailsService의 loadByUsername 메서드를 호출하여 사용자 세부 정보를 가져온다.

loadByUsername 메서드는 데이터베이스, 외부 시스템, 볼트 등에서 사용자를 로드하도록 구현한다.


public interface UserDetailsManager extends UserDetailsService {
	void createUser(UserDetails user);
    void updateUser(UserDetails user);
    void deleteUser(String username);
    void changePassword(String oldPassword, String newPassword);
    boolean userExists(String username);
}

UserDetailsManager는 사용자를 관리하는 기능을 포함하기 위해 UserDetailsService 인터페이스를 확장하고 메서드를 추가한다.
대표적으로 InMemoryUserDetailsManager와 JdbcUserDetailsManager가 존재하며, JdbcUserDetailsManager는 데이터베이스에 저장된 사용자를 관리하고 JDBC를 통해 데이터베이스와 직접 연결된다.


요약

  • UserDetails 인터페이스는 스프링 시큐리티에서 사용자를 기술하는 데 이용된다.
  • UserDetailsService는 애플리케이션이 사용자 세부 정보를 얻는 방법을 설명하기 위해 스프링 시큐리티의 인증 아키텍쳐에서 구현해야 하는 인터페이스이다.
  • UserDetailsManager는 UserDetailsService를 확장하고 사용자 생성, 변경, 삭제와 관련된 동작을 추가한다.

대표적인 UserDetailsManager의 구현체

  • InMemoryUserDetailsManager
  • JdbcUserDetailsManager
  • LdapUserDetailsManager

0개의 댓글