인프런/스프링 시큐리티 완전 정복 [6.x 개정판]

1) 초기화 과정이해 - 프로젝트 설정, Spring Security 기본설정클래스, SecurityBuilder / SecurityConfigurer

backend dev 2024. 9. 30.

프로젝트 설정

 

-Spring boot 3.3.4 버전

- JDK 17 

- Gradle

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    }

spring security 의존성 추가

 

 

 

 

Spring Boot Security 자동설정

spring security 의존성을 추가하면 관련 라이브러리가 다운로드 되고 

 

그중 

자동 설정 클래스가 추가된다.

 

 

 

SpringBootWebSecurityConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {

    /**
     * The default configuration for web security. It relies on Spring Security's
     * content-negotiation strategy to determine what sort of authentication to use. If
     * the user specifies their own {@link SecurityFilterChain} bean, this will back-off
     * completely and the users should specify all the bits that they want to configure as
     * part of the custom security configuration.
     */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {

       @Bean
       @Order(SecurityProperties.BASIC_AUTH_ORDER)
       SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
          http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
          http.formLogin(withDefaults());
          http.httpBasic(withDefaults());
          return http.build();
       }

    }

    /**
     * Adds the {@link EnableWebSecurity @EnableWebSecurity} annotation if Spring Security
     * is on the classpath. This will make sure that the annotation is present with
     * default security auto-configuration and also if the user adds custom security and
     * forgets to add the annotation. If {@link EnableWebSecurity @EnableWebSecurity} has
     * already been added or if a bean with name
     * {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been configured by the user, this
     * will back-off.
     */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
    @ConditionalOnClass(EnableWebSecurity.class)
    @EnableWebSecurity
    static class WebSecurityEnablerConfiguration {

    }

}

해당 클래스로 인해 자동 설정의 의한 기본 보안 작동된다.

 

서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 이루어진다

 

별도의 설정이나 코드를 작성하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동되어 작동한다

 

- 기본적으로 모든 요청에 대하여 인증여부를 검증하고 인증이 승인되어야 자원에 접근이 가능하다

http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());

 

- 인증 방식은 폼 로그인 방식과 httpBasic 로그인 방식을 제공한다

- 인증을 시도할 수 있는 로그인 페이지가 자동적으로 생성되어 렌더링 된다

http.formLogin(withDefaults());
http.httpBasic(withDefaults());

 

- 인증 승인이 이루어질 수 있도록 한 개의 계정이 기본적으로 제공된다

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {

....


    public static class User {

            /**
             * Default user name.
             */
            private String name = "user";

            /**
             * Password for the default user name.
             */
            private String password = UUID.randomUUID().toString();

    ...


}

SecurityProperties 설정 클래스에서 생성

username : user

password : 랜덤 문자열

 

 

현재 이러한 설정가지고는 웹 서비스를 운영할 수 없다.

[ 계정이 여러개 필요할 수 있고, 계정마다 권한이 다를 수 있으므로 ]

 

그래서 어플리케이션에 맞게 커스텀한 보안설정을 진행할 것이다.

 

 

@RestController
public class IndexController {

    @GetMapping("/")
    public String index() {
        return "index";
    }

}

다음과 같은 테스트용 컨트롤러를 만들고

서버를 실행시켜보면

실행 로그에는 user 계정의 비밀번호가 보인다.

 

.

localhost:8080으로 request해도 /login으로 이동된다. -> 자동으로 어떤 request든 인증을 받도록 자동설정 되어있다

 

화면으로는 기본 설정으로 생긴 로그인 화면이 보일것이다.

화면으로는 기본 설정으로 생긴  로그인 화면이 보일것이다.

 

 

로그인 성공후 로그인이 발생한 url로 되돌려줄때 기본경로라면 ?continue라는 부분이 붙는 경우가 존재한다.

 

다시한번 localhost:8080을 호출한다면

 

로그인 페이지없이 잘 보이는것을 확인가능하다.

 

 

이러한 설정을 자동으로 해주는 클래스인 SpringBootWebSecurityConfiguration은 항상 실행되는것은 아니다.

 

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {

    /**
     * The default configuration for web security. It relies on Spring Security's
     * content-negotiation strategy to determine what sort of authentication to use. If
     * the user specifies their own {@link SecurityFilterChain} bean, this will back-off
     * completely and the users should specify all the bits that they want to configure as
     * part of the custom security configuration.
     */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {

       @Bean
       @Order(SecurityProperties.BASIC_AUTH_ORDER)
       SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
          http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
          http.formLogin(withDefaults());
          http.httpBasic(withDefaults());
          return http.build();
       }

    }

    /**
     * Adds the {@link EnableWebSecurity @EnableWebSecurity} annotation if Spring Security
     * is on the classpath. This will make sure that the annotation is present with
     * default security auto-configuration and also if the user adds custom security and
     * forgets to add the annotation. If {@link EnableWebSecurity @EnableWebSecurity} has
     * already been added or if a bean with name
     * {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been configured by the user, this
     * will back-off.
     */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
    @ConditionalOnClass(EnableWebSecurity.class)
    @EnableWebSecurity
    static class WebSecurityEnablerConfiguration {

    }

}

 

@ConditionalOnWebApplication(type = Type.SERVLET)

 

이 조건이 맞아야 SpringBootWebSecurityConfiguration가 실행된다.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnWebApplicationCondition.class)
public @interface ConditionalOnWebApplication {

    /**
     * The required type of the web application.
     * @return the required web application type
     */
    Type type() default Type.ANY;

    /**
     * Available application types.
     */
    enum Type {

       /**
        * Any web application will match.
        */
       ANY,

       /**
        * Only servlet-based web application will match.
        */
       SERVLET,

       /**
        * Only reactive-based web application will match.
        */
       REACTIVE

    }

}

이 코드에서

@Conditional(OnWebApplicationCondition.class)

 

 

@Conditinal은 컴포넌트의 Bean 등록여부에 조건을 달 수 있게하는 어노테이션이다.

 

OnWebApplicationCondition 클래스안에서 웹 어플리케이션이 맞는지 확인하는 로직이 들어있다.

 

그리고 

 

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {

    @Bean
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
       http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
       http.formLogin(withDefaults());
       http.httpBasic(withDefaults());
       return http.build();
    }

}

이 static class 또한

@ConditionalOnDefaultWebSecurity

이런 조건이 존재하고

 

조건을 살펴보면

class DefaultWebSecurityCondition extends AllNestedConditions {

    DefaultWebSecurityCondition() {
       super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
    static class Classes {

    }

    @ConditionalOnMissingBean({ SecurityFilterChain.class })
    static class Beans {

    }

}

 

@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })

클래스패스내에 해당 클래스가 존재하는가? [ 의존성 추가로 가져온 라이브러리까지 포함]

-> 저 두 클래스는 security 의존성 추가시 생성되는 클래스 [ 사용자가 security를 사용할 마음이 있다라고 보는것 ]

@ConditionalOnMissingBean({ SecurityFilterChain.class })

SecurityFilterChain 빈이 아직 정의되지 않았을 때

-> SecurityFilterChain 빈이 없다는 것은 사용자가 커스텀 보안 설정을 하지 않았다는 의미이고, 

이 경우 Spring Boot의 기본 보안 설정이 동작한다.

 

하지만 spring은 DefaultSecurityFilterChain라는 기본 시큐리티필터체인을 제공한다.

하지만 해당 기본시큐리티필터체인을 등록하기전 검사하는것이므로 상관없다.

 

HttpSecurity에 다음과 같이 기본필터체인이 생성되는 메소드가 있다.

@Override
protected DefaultSecurityFilterChain performBuild() {
    ExpressionUrlAuthorizationConfigurer<?> expressionConfigurer = getConfigurer(
          ExpressionUrlAuthorizationConfigurer.class);
    AuthorizeHttpRequestsConfigurer<?> httpConfigurer = getConfigurer(AuthorizeHttpRequestsConfigurer.class);
    boolean oneConfigurerPresent = expressionConfigurer == null ^ httpConfigurer == null;
    Assert.state((expressionConfigurer == null && httpConfigurer == null) || oneConfigurerPresent,
          "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one.");
    this.filters.sort(OrderComparator.INSTANCE);
    List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
    for (Filter filter : this.filters) {
       sortedFilters.add(((OrderedFilter) filter).filter);
    }
    return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}

 

@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin(withDefaults());
    http.httpBasic(withDefaults());
    return http.build();
}

 

 

http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());

authenticate : 증명하다 [ 인증 ]

authorize : 인가하다  [ 인가 ]

 

[모든 http request에 대해 인증을 해야 인가를 해준다는 설정]

 

http.formLogin(withDefaults());

 

  • 사용자에게 웹 브라우저를 통해 접근할 수 있는 로그인 페이지를 제공합니다.
  • 기본적으로 "/login" 경로에 로그인 폼을 생성합니다.
  • 사용자 이름과 비밀번호를 입력할 수 있는 HTML 폼을 제공합니다.
  • 주로 웹 애플리케이션의 사용자 인터페이스를 통한 인증에 사용됩니다.

Form Login은 세션 기반으로 동작하며, 로그인 후 세션을 유지합니다.

 

 

http.httpBasic(withDefaults());

 

HTTP Basic 인증은 주로 API 요청이나 프로그래밍 방식의 접근에 사용됩니다.

 

  • user:password는 Base64로 인코딩되어 Authorization 헤더에 포함됩니다.
  • 클라이언트는 이 인증 정보를 모든 요청에 포함시켜 서버에 전송합니다.
  • 서버는 이 헤더를 해석하여 사용자를 인증합니다.

 

HTTP Basic은 매 요청마다 인증 정보를 전송하므로, HTTPS를 사용하여 보안을 강화해야 합니다.

 

Spring Security 기본설정은

두 방식을 함께 구현함으로써,

웹 애플리케이션은 일반 사용자와 API 클라이언트 모두에게 적절한 인증 방식을 제공할 수 있습니다.

 


SecurityBuilder / SecurityConfigurer

 

SecurityBuilder는 빌더 클래스로서 웹 보안을 구성하는 빈 객체와 설정클래스들을 생성하는 역할을 하며

대표적으로 WebSecurity, HttpSecurity [ 구현체 ] 가 있다

 

SecurityConfigurer 는 Http 요청과 관련된 보안처리를 담당하는 필터들을 생성하고 여러 초기화 설정에 관여한다

[ Spring Security는 인증,인가를 필터를 이용해서 처리하는 필터 기반 보안 프레임워크이다. ]

 

SecurityBuilderSecurityConfigurer를 참조하고 있으며
인증 및 인가 초기화 작업은 SecurityConfigurer에 의해 진행된다.
[ 초기화 작업 => 필요한 스프링빈 생성 및 등록,
SecurityBuilder가 SecurityConfigurer를 사용해서 인증 및 인가 초기화 작업을 한다. ]

SecurityBuilder가 SecurityConfigurer를 참조[사용]하고 있다.

 

SecurityBuilder / SecurityConfigurer를 이용한 Spring Security 초기화 과정

@AutoConfiguration

 

1. 스프링 자동설정이 진행되면서  SecurityBuilder를 생성한다. [빌더 클래스 생성 ]

2. SecurityBuilder가 SecurityConfigurer를 생성한다. [설정 클래스 생성 ]

public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {

    /**
     * Initialize the {@link SecurityBuilder}. Here only shared state should be created
     * and modified, but not properties on the {@link SecurityBuilder} used for building
     * the object. This ensures that the {@link #configure(SecurityBuilder)} method uses
     * the correct shared objects when building. Configurers should be applied here.
     * @param builder
     * @throws Exception
     */
    void init(B builder) throws Exception;

    /**
     * Configure the {@link SecurityBuilder} by setting the necessary properties on the
     * {@link SecurityBuilder}.
     * @param builder
     * @throws Exception
     */
    void configure(B builder) throws Exception;

}

3. SecurityConfigurer가 SecurityBuilder를 매개변수로 받고 init, configure 메소드를 이용하여 초기화 작업을 진행한다.

[ 이 작업에서 필터도 생성한다. ]

 

 

다시한번 과정 확인

 

 

SecurityBuilder의 구현체인 HttpSecurity가 SecurityConfigurer의 구현체를 생성한다. [ 종류가 많다. ]

해당 설정 클래스의 init, configure 메소드를 이용하여 초기화 작업을 진행한다.

초기화 작업이 진행되는 중 필터도 생성된다.

[ Configurer 마다 필터가 존재하고 그 필터들을 다 생성한다. ]

[ 각 Configurer는 관련된 필터 생성 (예: FormLoginConfigurer는 UsernamePasswordAuthenticationFilter 생성) ]

[ 각 Configurer는 특정 보안 기능에 관련된 필터를 생성 ]

[ 생성된 필터들은 FilterChainProxy에 등록되어 보안 체인 구성 ]

 

 

HttpSecurityConfiguration 클래스

그 안에 HttpSecurity를 빈으로 등록하는 부분을 살펴보자.

@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
    LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
    AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
          this.objectPostProcessor, passwordEncoder);
    authenticationBuilder.parentAuthenticationManager(authenticationManager());
    authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
    HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
    WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
    webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
    // @formatter:off
    http
       .csrf(withDefaults())
       .addFilter(webAsyncManagerIntegrationFilter)
       .exceptionHandling(withDefaults())
       .headers(withDefaults())
       .sessionManagement(withDefaults())
       .securityContext(withDefaults())
       .requestCache(withDefaults())
       .anonymous(withDefaults())
       .servletApi(withDefaults())
       .apply(new DefaultLoginPageConfigurer<>());
    http.logout(withDefaults());
    // @formatter:on
    applyCorsIfAvailable(http);
    applyDefaultConfigurers(http);
    return http;
}

스코프는 프로토타입

[Scope 관련 게시글 https://keeeeeepgoing.tistory.com/157]

 

HttpSecurity 초기화작업

http
    .csrf(withDefaults())
    .addFilter(webAsyncManagerIntegrationFilter)
    .exceptionHandling(withDefaults())
    .headers(withDefaults())
    .sessionManagement(withDefaults())
    .securityContext(withDefaults())
    .requestCache(withDefaults())
    .anonymous(withDefaults())
    .servletApi(withDefaults())
    .apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());

 

자세한 내용은 다음에 배울것이다.

.csrf(withDefaults())
public HttpSecurity csrf(Customizer<CsrfConfigurer<HttpSecurity>> csrfCustomizer) throws Exception {
    ApplicationContext context = getContext();
    csrfCustomizer.customize(getOrApply(new CsrfConfigurer<>(context)));
    return HttpSecurity.this;
}

csrf() 메소드를 예시로 보면 

new CsrfConfigurer<>(context))

CsrfConfigurer을 이용하여 설정해주는것이 보이는데 

CsrfConfigurer를 타고 올라가보면 SecurityConfigurer를 상속받은 추상클래스를 구현하고있는것을 확인가능하다.

 

http
    .csrf(withDefaults())
    .addFilter(webAsyncManagerIntegrationFilter)
    .exceptionHandling(withDefaults())
    .headers(withDefaults())
    .sessionManagement(withDefaults())
    .securityContext(withDefaults())
    .requestCache(withDefaults())
    .anonymous(withDefaults())
    .servletApi(withDefaults())
    .apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());
// @formatter:on
applyCorsIfAvailable(http);
applyDefaultConfigurers(http);
return http;

이렇게 각각의 configurer를 통해 설정해주고 httpSecurity를 빈으로 등록해준다.

 

Debug를 통해 HttpSecurity를 살펴보면

많은 configurer를 통해 설정되어있는걸 확인가능하다.


formLogin또한 configurer로 설정하는 모습

[뭔가 설정을 한다하면 configurer를 이용한다.]

 

@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin(withDefaults());
    http.httpBasic(withDefaults());
    return http.build();
}

 

이제 http.build()가 되면 httpSecurity의 configurer안의 init,configure 메소드를 실행시켜 초기화 작업이 이루어진다.

 

댓글