study/spring-security-in-action

3장 사용자 관리

fd27 2023. 9. 2. 19:52

이 블로그 내용은 아래 도서를 학습한 내용입니다.
학습 도서 정보
스프링 시큐리티 인 액션(Spring Security in Action)
로렌티우 스필카 지음
최민석 옮김
위키북스


  • userDetails 인터페이스로 사용자 기술하기
  • 인증 흐름에 UserDetailsService 이용하기
  • UserDetailsService의 맞춤형 구현 만들기
  • UserDetailsManager의 맞춤형 구현 만들기
  • 인증 흐름에 JdbcUserDetailsManager 이용하기

 
UserDetailsService에 대해서

  • 스프링 시큐리티에서 사용자를 기술하는 UserDetails
  • 사용자가 실행할 수 있는 작업을 정의하는 GrantedAuthority
  • UserDetailsService 계약을 확장하는 UserDetailsManager. 상속된 동작 외에 사용자 만들기, 사용자의 암호 수정이나 삭제 등의 작업도 지원한다.

 
UserDetailsService 및 PasswordEncoder에 대해서

  • 스프링 시큐리티에 있는 구현과 이를 이용하는 방법
  • 계약을 위한 맞춤형 구현을 정의하는 방법과 시기
  • 실제 애플리케이션에서 볼 수 있는 인터페이스를 구현하는 방법
  • 이러한 인터페이스 이용의 모범 사례

 

3.1 스프링 시큐리티의 인증 구현

UserDetailsService는 사용자 이름으로 사용자를 검색하는 역할만 한다.
UserDetailsManager는 대부분의 애플리케이션에 필요한 사용자 추가, 수정, 삭제 작업을 추가한다.
UserDetails로 계약을 구현한다.
GrantedAuthority는 이용 권리의 집합이다.
 
사용자를 인증하는 기능만 필요한 경우 UserDetailsService 계약만 구현하면 필요한 기능을 제공할 수 있다.
사용자를 관리하려면 UserDetailsService 및 UserDetailsManager 구성요소에 사용자를 나타내는 방법이 필요하다.
두 계약간 분리는 인터페이스 분리 원칙의 훌륭한 예다.

 

3.2 사용자 기술하기

스프링 시큐리티에서 사용자 정의는 UserDetails 계약을 준수해야 한다. UserDetails 계약은 스프링 시큐리티가 이해하는 방식으로 사용자를 타나낸다.

3.2.1 UserDetails 계약의 정의 이해하기

public interface UserDetails extends Serializable {
// 앱 사용자가 수행할 수 있는 작업을 GrantedAuthority 인스턴스의 컬랙션으로 반환
    Collection<? extends GrantedAuthority> getAuthorities();

// 사용자 자격 증명을 반환하는 두 메서드
    String getPassword();

    String getUsername();
    
    
// 사용자 계정을 필요에 따라 활성화 또는 비활성화하는 네 메서드
// 애플리케이션에서 네 메서드의 기능을 구현할 필요가 없다면 네 메서드가 true를 반환하게 하면 된다.
    boolean isAccountNonExpired(); // 계정 만료

    boolean isAccountNonLocked(); // 계정 잠금

    boolean isCredentialsNonExpired(); // 자격 증명 만료

    boolean isEnabled(); // 계정 비활성화
}

 

3.2.2 GrantedAuthority 계약 살펴보기

public interface GrantedAuthority extends Serializable {
    String getAuthority();
}

String 형식의 권한 이름 만들기

GrantedAuthority g1 = ()-> "READ";
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");

 

3.2.3 최소한의 UserDetails 구현 작성

public class User implements UserDetails {

    private final String username;
    private final String password;
    private final String authority;

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

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

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

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

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

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

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

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

3.2.4 빌더를 이용해 UserDetails 형식의 인스턴스 만들기

일부 단순한 애플리케이션에는 UserDetails 인터페이스의 맞춤형 구현이 필요 없다.
 

        // user 빌더 클래스로 사용자 구성
        UserDetails u = User.withUsername("bill")
                .password("12345")
                .authorities("read", "write")
                .accountExpired(false)
                .disabled(true)
                .build();

        // User.UserBuilder 인스턴스 만들기
        User.UserBuilder builder1 = User.withUsername("bill");

        UserDetails u1 = builder1
                .password("12345")
                .authorities("read", "write")
                .passwordEncoder(p -> encode(p)) // 암호 인코더는 인코딩을 수행하는 함수에 불과하다.
                .accountExpired(false)
                .disabled(true)
                .build();

        // 기존의 UserDetails 인스턴스에서 사용자를 만들 수도 있다.
        User.UserBuilder builder2 = User.withUserDetails(u);

        UserDetails u2 = builder2.build();

 

3.2.5 사용자와 연관된 여러 책임 결합

JPA 사용시 User 엔티티와 스프링 시큐리티의 User Details 구현체를 함께 구현하면 복잡하다.
SecurityUser 클래스를 구현해 User 엔티티를 래핑한다.
 

@Entity
@Getter
public class User {
    @Id
    private int id;
    private String username;
    private String password;
    private String authority;
}

public class SecurityUser implements UserDetails {

    private final User user;

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

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

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

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

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

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

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

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

3.3 스프링 시큐리티가 사용자를 관리하는 방법 지정

3.3.1 UserDetailsService 계약의 이해

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

3.3.2 UserDetailsService 계약 구현

인메모리로 UserDetailsService 구현

public class User implements UserDetails {

    private final String username;
    private final String password;
    private final String authority;

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

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

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
public class InMemoryUserDetailsService implements UserDetailsService {

    private final List<UserDetails> users;

    public InMemoryUserDetailsService(List<UserDetails> users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return users.stream()
                .filter(u -> u.getUsername().equals(username))
                .findFirst()
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
}
@Configuration
public class ProjectConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails u = new User("john", "12345", "read");
        List<UserDetails> users = List.of(u);
        return new InMemoryUserDetailsService(users);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}
curl -u john:12345 http://localhost:8080/hello

3.3.3 UserDetailsManager 계약 구현

public interface UserDetailsManager extends UserDetailsService {
    void createUser(UserDetails var1);

    void updateUser(UserDetails var1);

    void deleteUser(String var1);

    void changePassword(String var1, String var2);

    boolean userExists(String var1);
}

사용자 관리에 JdbcUserDetailsManager 이용

 

create schema spring;

CREATE TABLE IF NOT EXISTS `spring`.`users` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(45) NULL,
  `password` VARCHAR(45) NULL,
  `enabled` INT NOT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE IF NOT EXISTS `spring`.`authorities` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(45) NULL,
  `authority` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

INSERT INTO `spring`.`authorities` VALUES (NULL, 'john', 'write');
INSERT INTO `spring`.`users` VALUES (NULL, 'john', '12345', '1');
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Bean
  public UserDetailsService userDetailsService(DataSource dataSource) {
    String usersByUsernameQuery = "select username, password, enabled from spring.users where username = ?";
    String authsByUserQuery = "select username, authority from spring.authorities where username = ?";
    var userDetailsManager = new JdbcUserDetailsManager(dataSource);
    userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
    userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
    return userDetailsManager;

  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }

}

사용자 관리에 LdapUserDetailsManager 이용

실제 LDAP 서버를 사용할 수 없으므로 임베디드로 LDAP 서버를 설정한다.
 
LDIF (DATA Interchange Format) 파일 설정
 
src/main/resources/server.ldif

dn: dc=springframework,dc=org
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: springframework

dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: uid=john,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: John
sn: John
uid: john
userPassword: 12345
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-ldap</artifactId>
        </dependency>
        <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
        </dependency>

application.properties 파일
 

spring.ldap.embedded.ldif=classpath:server.ldif
spring.ldap.embedded.base-dn=dc=springframework,dc=org
spring.ldap.embedded.port=33389
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        // 컨텍스트 소스를 생성해 LDAP 서버의 주소 지정
        var cs = new DefaultSpringSecurityContextSource("ldap://127.0.0.1:33389/dc=springframework,dc=org");
        cs.afterPropertiesSet();

        // LdapuserDetailsManager 인스턴스 생성
        LdapUserDetailsManager manager = new LdapUserDetailsManager(cs);
        // 사용자 이름 매퍼를 설정해 LdapUserDetailsManager에 사용자를 검색 하는 방법 지시
        manager.setUsernameMapper(
                new DefaultLdapUsernameToDnMapper("ou=groups", "uid"));
        // 앱이 사용자를 검색하는 데 필요한 그룹 검색 기준 설정
        manager.setGroupSearchBase("ou=groups");
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

}

요약

  • UserDetails 인터페이스는 스프링 시큐리티에서 사용자를 기술하는 데 이용되는 계약이다.
  • UserDetailsService 인터페이스는 애플리케이션이 사용자 세부 정보를 얻는 방법을 설명하기 위해 스프링 시큐리티의 인증 아키텍처에서 구현해야 하는 계약이다.
  • UserDetailsManager 인터페이스는 UserDetailsSErvice를 확장하고 사용자 생성, 변경, 삭제와 관련된 동작을 추가한다.
  • 스프링 시큐리티는 UserDetailsManager 계약의 여러 구현을 제공한다. 이러한 구현에는 InMemoryUserDetailsManager, JdbcUserDetailsManager, LdapUserDetailsManager가 있다.
  • JdbcUserDetailsManager는 JDBC를 직접 이용하므로 애플리케이션이 다른 프레임워크에 고정되지 않는다는 이점이 있다.