1114 ~ 1127


# 1114 ~ 1127

# 1114 - 도메인 주도 설계의 모듈 구성(패키지 구성)

# 계층 기반 패키지

가장 단순한 첫번째 설계원칙인 수평 계층형 아키텍처

  • 기술적인 관점에서 해당 코드가 하는 일에 기반해 그 코드를 분할. 보통 웹베이스 코딩을 하게 되면 MVC, MVP 패턴으로 개발을 하게 되는데 이도 계층 기반 패키지라고 부르며 각 계층은 유사한 종류의 것들을 묶는 도구로 사용
  • 이 전형적인 아키텍처에서는 사용 인터페이스, 업무규칙, 영속성코드를 계층이 각각 하나씩 존재하며, 임의의 N계층은 반드시 N+1계층에만 의존 해야한다.

# 기능(도메인) 기반 패키지

기능 기반 패키지는 서로 연관된 기능, 도메인 개념 또는 Aggregate Root에 기반하여 수직의 얇은 조각으로 코드를 나누는 방식.
특정 기능을 지칭하는 하나의 패키지 내에 모든 타입과, 개념을 반영해서 구현.

  • 인터페이스와 클래스가 모두 단 하나의 패키지에 속한다. 이로 인해 코드의 상위 수준 구조가 업무 도메인에 대해 무언가를 알려주게 된다. 따라서 이 코드베이스가 계층단위(웹, 서비스, 리포지토리)가 아니라 주문(업무 도메인)과 관련된 무언가를 한다는 것을 볼 수 있다.
  • 변경시 변경해야 할 코드가 모두 한 패키지 내에 응집력을 갖고 있기때문에 유리하다.

# 포트와 어댑터

엉클 밥에 따르면 포트와 어댑터 혹은 육각형 아키텍처, 경계, 컨트롤러, 엔티티 등의 방식으로 접근하는 이유는 업무/도메인에 초점을 둔 코드가 기술적인 세부 구현과 독립적이며 분리된 아키텍처를 만들기 위해서이다.

'내부' 영역은 도메인 개념을 모두 포함하는 반면, '외부'영역은 UI, 데이터베이스, 서드파티등과의 상호작용을 포함한다. 여기서 중요한 규칙은 '외부'가 '내부'에 의존하며, 그 반대는 불가능하다.

  • 도메인 주도 설계에서는 OrderRepository는 Orders라는 명칭으로 바꿔줘야 한다. 그 이유는 도메인에 대해 논의할때 '주문'에 대해 말하는것이지 '주문 리포지토리'에 대해 말하는 것이 아니기 때문이다.

# 컴포넌트 기반 패키지

컴포넌트 기반 패키지는 마틴 파울러가 제시하는 방법으로, 지금까지 방식을 혼합한 것으로 큰 단위의 단일 컴포넌트와 관련된 모든 책임을 하나의 패키지로 묶는데 주안점을 두고 있다.
이 접근법은 서비스 중심적인 시각으로 소프트웨어 시스템을 바라 보며, 마이크로서비스 아키텍처가 가진 시각과도 동일하다.

  • 여기서 말하는 '컴포넌트'에 대해 마틴파울러는 "컴포넌트는 멋지고 깔끔한 인터페이스로 감싸진 연관된 기능들의 묶음으로, 애플리케이션과 같은 실행 환경 내부에 존재한다."로 정의하고 있다.
  • 컴포넌트 기반 패키지 접근법의 주된 이점은 주문과 관련된 무언가를 코딩해야 할 때 오직, OrdersComponent만 둘러보면 된다는 점이다. 이 컴포넌트 내부에서 관심사의 분리는 여전히 유효하며, 컴포넌트 구현과 관련된 세부사항은 사용자가 알 필요가 없다.
  • 이는 마이크로 서비스나 서비스 지향 아키텍처를 적용했을 때 얻는 이점과도 유사하다. 따라서 모노리틱 애플리케이션에서 컴포넌트를 잘 정의하면 결합 분리 모드를 통해 마이크로 서비스 아키텍처로 가기 위한 발판으로 삼을 수 있다.

# 1116 - Spring Security without the WebSecurityConfigurerAdapter

Spring Security 5.7.0-M2 부터는 구성 요소 기반 보안 설정으로 변경된다. 따라서 WebSecurityConfigurerAdapter 상속 후, configure 메소드를 오버라이딩 하여 설정하는 방식에서 SecurityFilterChain를 빈으로 등록하는 방식을 권장한다.

# HttpSecurity 구성 비교

  • WebSecurityConfigurerAdapter의 configure 메소드를 오버라이딩
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }

}
  • SecurityFilterChain 빈 등록
@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

# LDAP란?

LDAP(Lightweight Directory Access Protocol)란, 네트워크 상에서 조직이나 개인정보 혹은 파일이나 디바이스 정보 등을 찾아보는 것을 가능하게 만든 소프트웨어 프로토콜이다.

# JDBC 인증 설정 비교

  • WebSecurityConfigurerAdapter 상속, configure(AuthenticationManagerBuilder auth) 메소드 오버라이딩하여 설정
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        auth.jdbcAuthentication()
            .withDefaultSchema()
            .dataSource(dataSource())
            .withUser(user);
    }
}
  • JdbcUserDetailsManager 빈을 등록하는 방식
@Configuration
public class SecurityConfiguration {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
            .build();
    }

    @Bean
    public UserDetailsManager users(DataSource dataSource) {
        // 이 예에서는 User.withDefaultPasswordEncoder()가독성을 위해 방법을 사용
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
        users.createUser(user);
        return users;
    }
}

https://blog.naver.com/PostView.naver?blogId=h850415&logNo=222755455272&parentCategoryNo=&categoryNo=37&viewDate=&isShowPopularPosts=true&from=search
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter


# 1123 - Spring Security 구조

image

# Spring Security 주요 모듈

image

# [ Security ContextHolder ]

SecurityContextHolder는 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다. SecurityContextHolder는 기본적으로 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 방법과 SecurityContextHolder.MODE_THREADLOCAL 방법을 제공한다.

# [ SecurityContext ]

Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.

# [ Authentication ]

Authentication는 현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다. Authentication 객체는 Security Context에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근할 수 있다.

public interface Authentication extends Principal, Serializable {  
    // 현재 사용자의 권한 목록을 가져옴  
    Collection<? extends GrantedAuthority> getAuthorities();  
  
    // credentials(주로 비밀번호)을 가져옴  
    Object getCredentials();  
  
    Object getDetails();  
  
    // Principal 객체를 가져옴.  
    Object getPrincipal();  
  
    // 인증 여부를 가져옴  
    boolean isAuthenticated();  
  
    // 인증 여부를 설정함  
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;  
}

# [ UsernamePasswordAuthenticationToken ]

UsernamePasswordAuthenticationToken은 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, User의 ID가 Principal 역할을 하고, Password가 Credential의 역할을 한다. UsernamePasswordAuthenticationToken의 첫 번째 생성자는 인증전의 객체를 생성하고, 두번째 생성자는 인증이 완료된 객체를 생성한다.

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {  
    // 주로 사용자의 ID에 해당함  
    private final Object principal;  
    // 주로 사용자의 PW에 해당함  
    private Object credentials;  
  
    // 인증 완료 전의 객체 생성  
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {  
        super(null);  
        this.principal = principal;  
        this.credentials = credentials;  
        setAuthenticated(false);  
    }  
  
    // 인증 완료 후의 객체 생성  
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,  
                                               Collection<? extends GrantedAuthority> authorities) {  
        super(authorities);  
        this.principal = principal;  
        this.credentials = credentials;  
        super.setAuthenticated(true); // must use super, as we override  
    }  
}  
  
  
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {  
}

# [ AuthenticationProvider ]

AuthentivationProvider에서는 실제 인증에 대한 부분을 처리하는데, 인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다. 아래와 같은 AuthenticationProvider 인터페이스를 구현해서 Custom한 AuthenticationProvider을 작성해서 AuthenticationManager에 등록하면 된다.

public interface AuthenticationProvider {  
  
    // 인증 전의 Authenticaion 객체를 받아서 인증된 Authentication 객체를 반환  
    Authentication authenticate(Authentication var1) throws AuthenticationException;  
  
    boolean supports(Class<?> var1);  
  
}

# [ Authentication Manager ]

인증에 대한 부분은 SpringSecurity의 AuthenticationManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다. 인증이 성공하면 2번째 생성자를 이용해 인증이 성공한(isAuthenticated=true)객체를 생성하여 Security Context에 저장한다. 그리고 인증 상태를 유지하기 위해 세션에 보관하며, 인증이 실패한 경우에는 AuthenticationException를 발생시킨다.

public interface AuthenticationManager {  
    Authentication authenticate(Authentication authentication)  
            throws AuthenticationException;  
}

AuthenticationManager를 implements한 ProviderManager는 실제 인증 과정에 대한 로직을 가지고 있는 AuthenticationProvider를 List로 가지고 있으며, ProviderManager는 for문을 통해 모든 provider를 조회하면서 authenticate 처리를 한다.

public class ProviderManager implements AuthenticationManager, MessageSourceAware,  
        InitializingBean {  
    public List<AuthenticationProvider> getProviders() {  
        return providers;  
    }  
    public Authentication authenticate(Authentication authentication)  
            throws AuthenticationException {  
        Class<? extends Authentication> toTest = authentication.getClass();  
        AuthenticationException lastException = null;  
        Authentication result = null;  
        boolean debug = logger.isDebugEnabled();  
        //for문으로 모든 provider를 순회하여 처리하고 result가 나올 때까지 반복한다.  
        for (AuthenticationProvider provider : getProviders()) {  
        ....  
            try {  
                result = provider.authenticate(authentication);  
  
                if (result != null) {  
                    copyDetails(authentication, result);  
                    break;                }  
            }  
            catch (AccountStatusException e) {  
                prepareException(e, authentication);  
                // SEC-546: Avoid polling additional providers if auth failure is due to  
                // invalid account status                throw e;  
            }  
        ....  
        }  
        throw lastException;  
    }  
}

위에서 설명한 ProviderManager에 우리가 직접 구현한 CustomAuthenticationProvider를 등록하는 방법은 SecurityConfig에서 빈으로 등록할 수 있다.

# [ UserDetails ]

인증에 성공하여 생성된 UserDetails 객체는 Authentication 객체를 구현한 UsernamePasswordAuthenticationToken을 생성하기 위해 사용된다. UserDetails 인터페이스를 살펴보면 아래와 같이 정보를 반환하는 메소드를 가지고 있다. UserDetails 인터페이스의 경우 직접 개발한 UserVO 모델에 UserDetails를 implements하여 이를 처리하거나 UserDetailsVO에 UserDetails를 implements하여 처리할 수 있다.

public interface UserDetails extends Serializable {  
  
    Collection<? extends GrantedAuthority> getAuthorities();  
  
    String getPassword();  
  
    String getUsername();  
  
    boolean isAccountNonExpired();  
  
    boolean isAccountNonLocked();  
  
    boolean isCredentialsNonExpired();  
  
    boolean isEnabled();  
  
}

# [ UserDetailsService ]

UserDetailsService 인터페이스는 UserDetails 객체를 반환하는 단 하나의 메소드를 가지고 있는데, 일반적으로 이를 구현한 클래스의 내부에 UserRepository를 주입받아 DB와 연결하여 처리한다. UserDetails 인터페이스는 아래와 같다.

public interface UserDetailsService { 

UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; 

}
Last update: November 28, 2022 17:30
Contributors: jaesungahn91