본문 바로가기
책을 읽겠습니다!/Spring Security in Action

[Spring Security in Action] 2장 안녕! 스프링 시큐리티!!!!

by Unagi_zoso 2023. 9. 18.

스프링 부트는 미리 준비된 구성을 제공하므로 모든 구성을 작성하는 대신 자신의 구현과 일치하지 않는 구성만 재정의하면 된다.
이 접근법을 설정보다 관습(convention-over-configuration)이라고 한다.

모놀리식 아키텍처에서는 한 번 설정하고나면 다시 건드는 일이 드문데 서비스 지향 아키텍처가 유행하고 이러한 설정부분을 여러번 작업해야하는 일이 잦아졌다. 이러한 이유로 최신 앱, 마이크로서비스용 앱을 개발하는 데 스프링 부트가 더욱 인기를 얻었다.

스프링부트는 (@SpringBootApplicatioin 어노테이션이 지정된 클래스가 포함된 패키지(및 하위 패키지)의 구성 요소만 검색한다. 주 패키지 외부에서 스프링의 스테레오타입 구성 요소로 클래스에 어노테이션을 지정하려면 @ComponentScan 어노테이션으로 위치를 명시적으로 선언해야한다.

기본적으로 스프링 시큐리티 종속성을 두고 실행을 하면 자동적으로 HTTP Basic 인증을 위한 암호를 준다.
패스워드 없이 그냥 요청을 할 시 UnAuthorized 401을 받게 된다. 이는 인증 실패를 의미한다. 권한 부여 실패는 HTTP 403 금지된 상태를 이용하는 것이 좋다. 일반적으로 HTTP 403은 서버가 요청의 호출자를 식별했지만 이 호출자에게 이용 권리가 없다는 의미다.

  • HTTP Basic 인증을 이용한 엔드포인트 호출

: 문자열을 Base64로 인코딩하고 엔드포인트에 접근하면 올바른 결과를 받을 수 있다.

  curl -H "Authorization: Basic 'encoded string' end-point

  1. 인증 필터가 요청을 가로챈다.
  2. 인증 책임이 인증 관리자에 위임된다.
  3. 인증 관리자는 인증 논리를 구현하는 인증 공급자를 이용한다.
  4. 인증 공급자는 사용자 세부 정보 서비스로 사용자를 찾고 암호 인코더로 암호를 검증한다.
  5. 인증 결과가 필터에 반환된다.
  6. 인증된 엔티티에 관한 세부 정보가 보안 컨텍스트에 저장된다.
  • 인증 필터는 인증 요청을 인증 관리자에 위임하고 응답을 바탕으로 보안 컨텍스트를 구성한다.
  • 인증 관리자는 인증 공급자를 이용해 인증을 처리한다.
  • 인증 공급자는 인증 논리를 구현한다.
  • 인증 공급자는 사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용한다.
  • 인증 공급자는 암호 관리를 구현하는 암호 인코더를 인증 논리에 이용하다.
  • 보안 컨텍스트는 인증 프로세스 후 인증 데이터를 유지한다.

자동으로 구성되는 다음 빈에 관해 알아보자.

  • UserDetailsService
  • PasswordEncoder

인증 공급자는 이러한 빈을 이용해 사용자를 찾고 암호를 확인한다. 인증에 필요한 자격 증명을 제공하는 방법부터 시작해보자.

사용자에 관한 세부 정보는 스프링 시큐리티로 UserDetailsService 계약을 구현하는 객체가 관리한다. 이 구현은 애플리케이션의 내부 메모리에 기본 자격 증명을 등록하는 일만 한다. 기본 자격 증명에서 사용자 이름은 'user' 기본 암호는 UUID 형식이며 암호는 스프링 컨텍스트가 로드될 때 자동으로 생성된다. 이 기본구현은 개념 증명의 역할을 하며 종속성이 작동하는 것을 확인해준다. 이 구현은 자격 증명을 메모리에 보과한다. 즉 애플리케이션은 자격 증명을 보존하지 않는다. 이 접근법은 예제나 개념 증명에 적합하지 운영할 때는 쓰지 말자.

다음으로 PasswordEncoder가 있다. PasswordEncoder는 두 가지 일을 한다.

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

UserDetailsService 객체와 마찬가지로, PasswordEncoder 객체도 Basic 인증 흐름에 꼭 필요하다. 가장 단순한 구현에서는 암호를 일반텍스트로 관리하고 인코딩하지 않는다. UserDetailsService의 기본 구현을 대체할 때는 PasswordEncoder도 지정해야한다.

스프링부트는 기본 HTTP Basic 접근 인증을 구성할 때 인증 방식도 선택하며 이는 가장 직관적인 접근 인증 방식이다. Basic 인증에서는 이름과 암호를 HTTP Authorizatioin 헤더를 통해 보내기만하면 된다. Bse64는 전송의 편의를 위한 인코딩 방법이고 암호화나 해싱 방법이 아니라 자격 증명을 가로채면 누구든지 볼 수 있다. 기밀을 위해 HTTPS를 함께 이용할 때가 아니면 HTTP Basic 인증은 이용하지 않는다.

AuthenticatioinProvider는 인증 논리를 정의하고 사용자와 암호의 관리를 위힘한다. AuthenticationProvider의 기본 구현은 UserDetailsService 및 PasswordEncoder에 제공된 기본 구현을 이용한다.

  • HTTP 및 HTTPS 비교

HTTPS를 통해서만 통신하자..
시스템에 HTTPS를 구성하는데는 여러 패턴이 있다. 개발자는 떄에 따라 애플리케이션 수준에 HTTPS를 구성하거나 서비스 메시를 이용할 수도 있고 인프라 수준에 HTTPS를 설정할 수 도 있다. 스프링부트에선 애플리케이션 수준에 손쉽게 HTTPS를 활성화할 수 있다.

이러한 모든 구성 시나리오에는 인증기관 CA이 서명한 인즈서가 필요하다. 인증서를 이용하면 엔드포인트를 호출하는 클라이언트에서 응답이 인증 서버에서 보낸 것이며 누구도 통신을 가로채지 않았다는 것을 알 수 있따. 인증서는 구매해야 하며 갱신이 필요하다. 애플리케이션 테스트 용도로만 HTTPS를 구성하려는거면 OpenSSL과 가은 툴을 이용해 자체 서명 인증서를 생성하면 된다.

기본 구성 재정의

UserDetailsService 구성 요소 재정의

UserDetailsService 형식의 맞춤형 빈을 정의해서 스프링 시큐리티에 있는 기본 구성 요소를 재정의하는 법

  • 직접 정의도 할 수 있다
  • 기존 구현된 것을 가져올 수 있다.
@Configuration
public class ProjectConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsService =
            new InMemoryUserDetailsManager();
        return userDetailsService;
    }
}

스프링 컨텍스트에 추가된 UserDetailsService 형식의 인스턴스를 이용한다.

  • 사용자가 없다.
  • PasswordEncoder가 없다.
  1. 자격 증명(사용자 이름 및 암호)이 있는 사용자를 하나 이상 만든다.
  2. 사용자를 UserDetailsService에서 관리하도록 추가한다.
  3. 주어진 암호를 UserDetailsService가 저장하고 관리하는 암호를 이용해 검증하는 PasswordEncoder 형식의 빈을 정의한다.

InMemoryUserDetailsManager의 인스턴스를 대상으로인증하는데 이용가능한 자격증명집합을 선언하고 추가한다. 미리 정의된 빌더를 이용해 UserDetails 형식의 객체를 만든다.
인스턴스를 만들 때는 사용자 이름과 암호, 그리고 하나 이상의 권한을 지정해야 한다. 권한은 해당 사용자에게 허용된 작업이며 아무 문자열이나 지정하면 된다.

@Configuration
public class ProjectConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsService =
            new InMemoryUserDetailsManager();

        var user = User.withUsername("john")
            .password("1245"0
            .authorities("read")
            .build();
        userDetailsService.createUser(user);

        return userDetailsService;
    }
}

재정의 시 PasswordEncoder도 선언해야한다.

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

NoOpPasswordEncoder는 암호에 암호화나 해시를 적용하지 않고 일반 텍스트처럼 처리한다. 암호를 비교할 때 String의 equals 메서드로 비교한다. 절대 쓰지마!!!

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

애플리케이션의 모든 엔드포인트를 보호할 필요는 없다. 이러한 적용 위해 WebSecurityConfigurerAdapter 클래스를 확장하는 것부터 시작해보자. 이 클래스를 확장하면 configure(HttpSecurity http) 메서드를 재정의할 수 있다.

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
        return  http.build();
    }

Bean으로 설정할 수도 있고 상속받아서 설정할 수도 있네.. 뭐가 이렇게 많아..

다른 방법으로 구성 설정

시큐리티의 구성을 작성할 때 혼도되는 점은 여러 가지 방법으로 같은 구성을 만들 수 있다는 것.

@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());
    }

컨텍스트에 빈을 추가하는 첫 옵션을 이용하면 필요할 가능성이 있는 다른 클래스에 값을 주입할 수 있다. 하지만 그럴 필요가 없을 때는 두 번째 옵션도 마찬가지로 좋다. 구성을 혼합하면 헷갈릴 수 있어 권장하지 않는다.

운영단계 애플리케이션은 상황이 ㄷ이터베이스에 저장하거나 다른 시스템에서 가져와야 한다.

AuthenticationProvider 구현 재정의

스프링 시큐리티 구성요소는 상당히 유연하다. 다양한 옵션을 선택할 수 있다. 구성 요소 작업을 위임하는 AuthenticationProvider도 맞춤 구성할 수 있다.

스프링 시큐리티는 세분화된 책임과 느슨한 결합으로 상당히 유연하다. 그래서 이러한 설계 또한 바꿀 수 있는데 이러한 접근은 솔루션을 복잡하게 만들 수 있어 주의 필요하다. UserDetailsService나 PasswordEncoder가 더 필요없도록 기본 AuthenticationProvider를 재정의할 수도 있다.

참고
@Component

  • 개발자가 직접 작성한 클래스를 빈으로 등록하고 싶을 때 사용

@Configuration

  • 개발자가 직접 제어가 불가능한 외부 라이브러리 또는 설정을 위한 클래스를 Bean으로 등록할 때 사용
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        if ("john".equals(username) &&
        "12435".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        } else {
            throw  new AuthenticationCredentialsNotFoundException("Error in authentication!");
        }
    }

YserDetailsSErvice 및 PasswordEncoder의 책임을 대체한다.

두 개의 빈을 이용해야 하는 건 아니지만 인증을 위해 사용자와 암호를 관리한다면 논리를 분리하는 것이 좋다. 인증 구현을 재정의할 때도 스프링 시큐리티 아키텍처의 설계를 따르는 것이 좋다.

고유한 AuthenticatonProvider를 구현하여 인증 논릴ㄹ 대체하는 방법이 유용할 수 있다.

이를 configure(AuthenticationManagerBuilder auth) 메서드에서 AuthenticationProvider를 등록할 수 있다.

프로젝트에 여러 구성 클래스 이용

앞서 구현한 여러 예제에서는 하나의 구성 클래스만 사용했다. 하지만 구성 클래스도 책임을 분리하는 것이 좋다. 이러한 분리가 필요한 이유는 구성이 복잡해지기 때문이다. 운영 단계 애플리케이션에는 당연히 복잡한 선언이 포함된다. 이해하기 쉽게 만들기 위해 둘 이상의 구성 클래스를 만드는 것이 좋다.

이 예제에서는 권한 부여 구성에서 사용자 관리 구성을 분리할 수 있다.

package com.example.securityy;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author : Unagi_zoso
 * @date : 2023-09-18
 */
public class UserManagementConfig {

    @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();
    }
}
@Configuration
public class WebAuthorizatonConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
        return  http.build();
    }
}

여기에서 UserManagermentConfig 클래스는 사용자 관리를 담당하는 UserDetailsService 및 PasswordEncoder의 두 빈만 포함한다. 이 클래스는 WebSecurityCon..Adapter 확장 못해 두 객체를 빈으로 구성

WebAuthorizationConfig는 확장해서 재정의한다.

댓글