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


 

  • PasswordEncoder의 구현 및 이용
  • 스프링 시큐리티 암호화 모듈에 있는 툴 이용

 

4.1 PasswordEncoder 계약의 이해

4.1.1 PasswordEncoder 계약의 정의

public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

 
encode: 암호의 해시를 제공하거나 암호화를 수행하는 일을 한다.
matches: 인코딩된 문자열이 원시 암호화 일치하는지 확인한다.
upgradeEncoding: true를 반환하도록 재정의 하면 인코딩된 암호를 보안 향상을 위해 다시 인코딩 한다.

4.1.2 PasswordEncoder 계약의 구현

PasswordEncoder의 가장 단순한 구현

public class PlainTextPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return rawPassword.equals(encodedPassword);
    }
}

SHA-512를 이용ㅇ하는 PasswordEncoder 구현

public class Sha512PasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return hashWithSHA512(rawPassword.toString());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(encode(rawPassword));
    }

    private String hashWithSHA512(String input) {
        StringBuilder result = new StringBuilder();
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            byte [] digested = md.digest(input.getBytes());
            for (byte b : digested) {
                result.append(Integer.toHexString(0xFF & b));
            }
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Bad algorithm");
        }
        return result.toString();
    }
}

 

  • NoOpPasswordEncoder: 암호를 인코딩하지 않고 일반 텍스트로 유지한다.
  • StandardPasswordEncoder: SHA-256을 이용해 암호를 해시한다. 이 구현은 이제 구식이며 새 구현에는 쓰지 말아야 한다.
  • Pbkdf2PasswordEncoder: PBKDF2를 이용한다
  • BCryptPasswordEncoder: bycrypt 강력 해싱 함수로 암호를 인코딩 한다.
  • SCryptPasswordENcoder: scrypt 해싱 함수로 암호를 인코딩 한다.

 

4.1.3 PasswordEncoder의 제공된 구현 선택

NoOpPasswordEncoder

PasswordEncoder p = NoOpPasswordEncoder.getInstance();

NoOpPasswordEncoder 클러스는 싱글톤으로 설계돼서 클래스 바깥에서는 생성자를 직접 호출할 수 없지만, getInstance() 메서드로 클래스의 인스턴스를 얻을 수 있다.
 

StandardPasswordEncoder

PasswordEncoder p = new StandardPasswordEncoder();
PasswordEncoder p = new StandardPasswordEncoder("secret");

비밀값은 생성자의 매개변수로 전달한다.
StandardPasswordEncoder는 이제 구식이므로 쓰지않는게 좋다.
 

Pbkdf2PasswordEncoder

PasswordEncoder p = new Pbkdf2PasswordEncoder();
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret");
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret", 185000, 256);

Params

  • 프로세스에 이용되는 키의 값
  • 암호 인코딩의 반복 횟수
  • 해시의 크기

두번째와 세번째 값을 늘릴수록 암호는 더 강력해진다. 그러나 성능에 영향을 주므로 신중하게 절충해야한다.
 

BCryptPasswordEncoder

PasswordEncoder p = new BCryptPasswordEncoder();
PasswordEncoder p = new BCryptPasswordEncoder(4);

SecureRandom s = SecureRandom.getInstanceStrong();
PasswordEncoder p = new BCryptPasswordEncoder(4, s);

Params

  • 로그라운드: 해싱 작업이 이용하는 반복 횟수에 영향을 준다. 반복 횟수는 2 로그라운드로 계산된다. 반복 횟수를 계산하기 위한 로그라운드 값은 4~31 사이어야 한다.
  • SecureRandom: 인코딩에 이용되는 인스턴스

반복을 결정하는 값과, 해시에 사용되는 인스턴스를 지정해준다는 말인듯
인수가 없는 인스턴스로 만들어서 사용해도 된다.
 

SCryptPasswordEncoder

PasswordEncoder p = new SCryptPasswordEncoder();
PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);

Params

  • CPU 비용
  • 메모리 비용
  • 병렬화 계수
  • 키 길이
  • 솔트 길이

 

4.1.4 DelegatingPasswordEncoder를 이용한 여러 인코딩 전략

DelegatingPasswordEncoder는 여러종류의 해시를 지원한다.
현재 사용되는 알고리즘에서 취약성이 발견되어 신규 등록 사용자의 자격 증명을 변경하고 싶지만, 기존 자격 증명을 변경하기 쉽지 않을때 사용할 수 있다.
 
DelegatingPasswordEncoder는 PasswordEncoder 인터페이스의 한 구현이며 자체 인코딩 알고리즘을 구현하는 대신 같은 계약의 다른 구현 인스턴스에 작업을 위임한다.
해시는 해당 해시를 의미하는 알고리즘의 이름을 나타내는 접두사로 시작한다. DelegatingPasswordEncoder는 암호의 접두사를 기준으로 올바른 PasswordEncoder 구현에 작업을 위임한다.

@Configuration
public class ProjectConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> encoders = new HashMap<>();

        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());

        // 첫 번째 인수로 기본 인코더를 지정
        // 접두가사 없으면 기본 인코더를 사용함
        return new DelegatingPasswordEncoder("bcrypt", encoders);
    }
}

{noop}12345 => NoOpPasswordEncoder 사용
{bcrypt}$2a$10... => BCryptPasswordEncoder 사용
 
스프링 시큐리티는 편의를 위해 모든 표준 제공 PasswordEncoder의 구현에 대한 맵을 가진 DelegatingPasswordEncoder를 생성하는 방법을 제공한다.

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

기본 인코더는 bcrypt이다.
 

인코딩, 암호화, 해싱

> 인코딩(Encoding)
주어진 입력에 대한 모든 변환을 의미한다. 예를 들어 문자열을 뒤집는 함수 x가 있을 때 x -> y를 ABCD에 적용하면 DCBA가 나온다.
 
> 암호화(Encryption)
출력을 얻기 위해 입력 값과 키를 모두 지정하는 특정한 유형의 인코딩이다. 키를 이용하면 나중에 누가 출력에서 입력을 얻는 함수를 호출할 수 있는지 선택할 수 있다. 암호화를 함수로 나타낸 가장 간단한 형식은 다음과 같다.
(x, y) -> y
여기에서 x는 입력이고 k는 키며 y는 암호화 결과다. 이 방식으로 알려진 함수에 키를 이용해 출력에서 입력을 얻을 수 있다.
(y, k) -> x
이를 역함수 복호화(Reverse Function Decryption)라고 한다. 암호화에 쓰는 키와 복호화에 쓰는 키가 같으면 이를 일반적으로 이를 대칭 키 라고 한다.
 
암호화((x, k1) -> y) 및 복호화 ((y, k2) -> x)에 다른 키를 쓰면 비대칭 키(Asymmetric Key)로 암호화가 수행된다고 말한다.
그리고 (k1, k2)를 키 쌍(Key Pair)이라고 한다. 암호화에 이용되는 k1을 공개 키(Public key)라고 하고 복호화에 이용되는 k2를 개인 키(Private Key)라고 한다. 이와 같이 개인 키의 소유자는 데이터를 복호화할 수 있다.
 
>  해싱(Hashing)
함수가 한 방향으로만 작동하는 특정한 유형의 인코딩이다. 따라서 해싱 함수의 출력 y에서 입력 x를 얻을 수 없다. 그러나 출력 y가 입력 x에 해당하는지 확인할 수 있는 방법이 반드시 있어야 하므로 해시는 인코딩과 일치를 위한 한 쌍의 함수로 볼 수 있다.
해싱 함수가 x -> y라면 일치 함수 (x, y) -> boolean도 있다.
때때로 해싱 함수는 입력에 임의의 값을 추가할 수도 있다: (x, k) -> y.
이 값을 솔트(Salt)라고 한다.
솔트는 함수를 더 강하게 만들어 결과에서 입력을 얻는 역함수의 적용 난도를 높인다.
 
 
스프링 시큐리티에서 인증 흐름을 위한 주 계약을 나타내는 인터페이스

  • UserDetails
    • 스프링 시큐리티가 관리하는 사용자를 나타낸다.
  • GrantedAuthority
    • 애플리케이션의 목적 내에서 사용자에게 허용되는 작업을 정의한다(예: 읽기, 쓰기, 삭제 등).
  • UserDetailsService
    • 사용자 이름으로 사용자 세부 정보를 검색하는 객체를 나타낸다.
  • UserDetailsManager
    • UserDetailsService의 더 구체적인 계약이다. 사용자 이름으로 사용자를 검색하는 것 외에도 사용자 컬렉션이나 특정 사용자를 변경할 수도 있다.
  • PasswordEncoder
    • 암호를 암호화 또는 해시하는 방법과 주어진 인코딩된 문자열을 일반 텍스트 암호와 비교하는 방법을 지정한다.

 

4.2 스프링 시큐리티 암호화 모듈에 관한 추가 정보

4.2.1 키 생성기 이용

4.2.2 암호화 복호화 작업에 암호기 이용

 

요약

  • PasswordEncoder는 인증 논리에서 암호를 처리하는 가장 중요한 책임을 담당한다.
  • 스프링 시큐리티는 해싱 알고리즘에 여러 대안을 제공하므로 필요한 구현을 선택하기만 하면 된다.
  • 스프링 시큐리티 암호화 모듈(SSCM)에는 키 생성기와 암호기를 구현하는 여러 대안이 있다.
  • 키 생성기는 암호화 알고리즘에 이용되는 키를 생성하도록 도와주는 유틸리티 객체다.
  • 암호기는 데이터 암호화와 복호화를 수행하도록 도와주는 유틸리티 객체다.

'study > spring-security-in-action' 카테고리의 다른 글

8장 권한 부여 구성: 제한 적용  (0) 2023.09.24
7장 권한 부여 구성: 액세스 제한  (0) 2023.09.14
5장 인증 구현  (0) 2023.09.03
3장 사용자 관리  (0) 2023.09.02
2장 안녕! 스프링 시큐리티  (0) 2023.09.02

이 블로그 내용은 아래 도서를 학습한 내용입니다.
학습 도서 정보
스프링 시큐리티 인 액션(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를 직접 이용하므로 애플리케이션이 다른 프레임워크에 고정되지 않는다는 이점이 있다.

'study > spring-security-in-action' 카테고리의 다른 글

8장 권한 부여 구성: 제한 적용  (0) 2023.09.24
7장 권한 부여 구성: 액세스 제한  (0) 2023.09.14
5장 인증 구현  (0) 2023.09.03
4장 암호 처리  (0) 2023.09.02
2장 안녕! 스프링 시큐리티  (0) 2023.09.02

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


 
1. 스프링 시큐리티와 웹 종속성만 있는 프로젝트를 만들어서 추가된 구성이 없으면 어떻게 작동하는지 확인한다. 이를 통해 인증과 권한 부여의 기본 구성에서 무엇을 기대해야할 지 이해할 수 있다.
2. 기본 구성을 재정의하고 맞춤형 사용자와 암호를 정의해서 프로젝트에 사용자 관리 기능을 추가한다.
3. 애플리케이션이 기본적으로 모든 엔드포인트를 인증하는 것을 확인하고 이 동작도 맞춤 구성할 수 있다는 것을 배운다.
4. 모범 사례를 이해할 수 있도록 동일한 구성에 대해 다른 스타일을 적용해 본다.
 

2.1 첫 번째 프로젝트 시작

아래와 같은 스프링 시큐리티 종속성을 추가하면, 스프링 시큐리티가 작동한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

스프링 시큐리티는 기본적으로 모든 엔드포인트를 보호한다.
올바른 자격증명을 제공하지 않고 HTTP를 호출하면 401 권한없음이 반환된다.
자격증명은 아래와 같이 전송한다.
 
방법1

curl -u user:49ceebbc-f593-4dcb-ac5d-5b1b5a15939b http://localhost:8080/hello

방법2

echo -n user:49ceebbc-f593-4dcb-ac5d-5b1b5a15939b | base64
curl -H "Authorization: dXNlcjo0OWNlZWJiYy1mNTkzLTRkY2ItYWM1ZC01YjFiNWExNTkzOWI=" http://localhost:8080/hello

2.2 기본 구성이란?

AuthenticationProvider는 인증공급자이다.
UserDetailsService는 사용자 세부 정보 서비스이다.
PasswordEncoder는 암호 인코더이다.

  • 인증 필터는 인증 요청을 인증 관리자에 위임하고 응답을 바탕으로 보안 컨텍스트를 구성한다.
  • 인증 관리자는 인증 공급자를 이용해 인증을 처리한다.
  • 인증 공급자는 인증 논리를 구현한다.
  • 인증 공급자는 사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용한다.
  • 보안 컨텍스트는 인증 프로세스 후 인증 데이터를 유지한다.

사용자 세부 정보 서비스는 UserDetailsService 계약을 구현하는 객체가 관리한다.
PasswordEncoder는 다음 두 가지 일을 한다.

  • 암호를 인코딩 한다.
  • 암호가 기존 인코딩과 일치하는지 확인한다.

2.3 기본 구성 재정의

2.3.1 UserDetailsService 구성 요소 재 정의

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsService = new InMemoryUserDetailsManager();
 
        var user = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();
 
        userDetailsService.createUser(user);
 
        return userDetailsService;
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
 
}
curl -u john:12345 http://localhost:8080/hello

2.3.2 엔드포인트 권한 부여 구성 재정의

...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
        http.authorizeRequests().anyRequest().authenticated(); // 모든 요청에 인증이 필요하다.
        http.authorizeRequests().anyRequest().permitAll(); // 모든 요청에 인증이 필요하다.
    }
...

 

2.3.3 다른 방법으로 구성 설정

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    var userDetailsService = new InMemoryUserDetailsManager();
 
    var user = User.withUsername("john")
            .password("12345")
            .authorities("read")
            .build();
 
    userDetailsService.createUser(user);
 
    auth.userDetailsService(userDetailsService)
        .passwordEncoder(NoOpPasswordEncoder.getInstance());
}

위와 같이 configure에서 UserDetailsService 및 PasswordEncoder를 설정하면 어디서 UserDetailsService 및 PasswordEncoder가 연결되어있는지 알기 쉽지 않다.
2.3.1에서 구성한 것과 같이 책임을 분리해서 작성해야 한다. (1책임당 1클래스로 구성하는게 좋다)
 

2.3.4 AuthenticationProvider 구현 재정의

AuthenticationProvider는 인증 논리를 구현하고 사용자 관리와 암호 관리를 각각 UserDetailsService 및 PasswordEncoder에 위임한다.

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
 
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // Principal 인터페이스의 getName() 메서드를 Authentication에서 상속받는다.
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());
 
 
        // 이 조건은 일반적으로 UserDetialsService 및 PasswordEncoder를 호출해서 사용자 이름과 암호를 테스트한다.
        if ("john".equals(username) && "12345".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
            throw new AuthenticationCredentialsNotFoundException("Error!");
        }
    }
 
    @Override
    public boolean supports(Class<?> authenticationType) {
        // Authentication 형식의 구현을 추가할 위치
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
    }
}

authentication(Authentication authentication) 메서드는 인증의 전체 논리를 나타낸다.
 
새로운 구성

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private CustomAuthenticationProvider authenticationProvider;
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider);
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

요약

  • 스프링 시큐리티를 애플리케이션의 종속성으로 추가하면 스프링 부트가 약간의 기본 구성을 제공한다.
  • 인증과 권한 부여를 위한 기본 구성요소인 UserDetailsService, PasswordEncoder, AuthenticationProvider를 구현했다.
  • User 클래스로 사용자를 정의할 수 있다. 사용자는 사용자 이름, 암호, 권한을 가져야 한다. 권한은 사용자가 애플리케이션의 컨텍스트에서 수행할 수 있는 작업을 지정한다.
  • 스프링 시큐리티는 userDetailsService의 간단한 구현인 InMemoryuserDetailsManager를 제공한다. UserDetailsService의 인스턴스와 같은 사용자를 추가해서 애플리케이션의 메모리에 사용자를 관리할 수 있다.
  • NoOpPasswordEncoder는 PasswordEncoder 계약을 구현하며 암호를 일반 텍스트로 처리한다. 이 구현은 학습 예제와 개념 증명에 적합하지만 운영 단계 애플리케이션에는 적합하지 않다.
  • AuthenticationProvider 계약을 이용해 애플리케이션의 맞춤형 인증 논리를 구현할 수 있다.
  • 구성을 작성하는 방법은 여러 가지가 있지만, 한 애플리케이션에서는 한 방법을 선택하고 고수해야 코드를 깔끔하고 이해하기 쉽게 만들 수 있다.

'study > spring-security-in-action' 카테고리의 다른 글

8장 권한 부여 구성: 제한 적용  (0) 2023.09.24
7장 권한 부여 구성: 액세스 제한  (0) 2023.09.14
5장 인증 구현  (0) 2023.09.03
4장 암호 처리  (0) 2023.09.02
3장 사용자 관리  (0) 2023.09.02

+ Recent posts