WebFlux セキュリティ

Spring Security の WebFlux サポートは WebFilter に依存しており、Spring WebFlux および Spring WebFlux.Fn でも同じように機能します。いくつかのサンプルアプリケーションがコードを示しています。

最小限の WebFlux セキュリティ構成

次のリストは、最小限の WebFlux セキュリティ構成を示しています。

最小限の WebFlux セキュリティ構成
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {

	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
			.username("user")
			.password("user")
			.roles("USER")
			.build();
		return new MapReactiveUserDetailsService(user);
	}
}
@Configuration
@EnableWebFluxSecurity
class HelloWebfluxSecurityConfig {

    @Bean
    fun userDetailsService(): ReactiveUserDetailsService {
        val userDetails = User.withDefaultPasswordEncoder()
                .username("user")
                .password("user")
                .roles("USER")
                .build()
        return MapReactiveUserDetailsService(userDetails)
    }
}

この構成は、フォームと HTTP の基本認証を提供し、任意のページにアクセスするために認証されたユーザーを要求する認可を設定し、デフォルトのログインページとデフォルトのログアウトページを設定し、セキュリティ関連の HTTP ヘッダーを設定し、CSRF 保護を追加します。

明示的な WebFlux セキュリティ構成

次のページは、最小限の WebFlux セキュリティ構成の明示的なバージョンを示しています。

明示的な WebFlux セキュリティ構成
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {

	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
			.username("user")
			.password("user")
			.roles("USER")
			.build();
		return new MapReactiveUserDetailsService(user);
	}

	@Bean
	public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
		http
			.authorizeExchange((authorize) -> authorize
			    .anyExchange().authenticated()
			)
			.httpBasic(withDefaults())
			.formLogin(withDefaults());
		return http.build();
	}
}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class HelloWebfluxSecurityConfig {

    @Bean
    fun userDetailsService(): ReactiveUserDetailsService {
        val userDetails = User.withDefaultPasswordEncoder()
                .username("user")
                .password("user")
                .roles("USER")
                .build()
        return MapReactiveUserDetailsService(userDetails)
    }

    @Bean
    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http {
            authorizeExchange {
                authorize(anyExchange, authenticated)
            }
            formLogin { }
            httpBasic { }
        }
    }
}
IDE がメソッドを常に自動インポートするとは限らず、コンパイルの問題が発生する可能性があるため、クラスで Kotlin DSL を有効にするには、必ず org.springframework.security.config.web.server.invoke 関数をインポートしてください。

この構成は、最小構成と同じものをすべて明示的に設定します。ここから、デフォルトをより簡単に変更できます。

 config/src/test/ ディレクトリの EnableWebFluxSecurity  [GitHub] (英語) を検索すると、単体テストで明示的な構成の例をさらに見つけることができます。

複数のチェーンサポート

複数の SecurityWebFilterChain インスタンスを構成して、RequestMatcher インスタンスごとに構成を分離できます。

例: /api で始まる URL の構成を分離できます:

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
static class MultiSecurityHttpConfig {

    @Order(Ordered.HIGHEST_PRECEDENCE)                                                      (1)
    @Bean
    SecurityWebFilterChain apiHttpSecurity(ServerHttpSecurity http) {
        http
            .securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/**"))      (2)
            .authorizeExchange((authorize) -> authorize
                .anyExchange().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);                           (3)
        return http.build();
    }

    @Bean
    SecurityWebFilterChain webHttpSecurity(ServerHttpSecurity http) {                       (4)
        http
            .authorizeExchange((authorize) -> authorize
                .anyExchange().authenticated()
            )
            .httpBasic(withDefaults());                                                     (5)
        return http.build();
    }

    @Bean
    ReactiveUserDetailsService userDetailsService() {
        return new MapReactiveUserDetailsService(
                PasswordEncodedUser.user(), PasswordEncodedUser.admin());
    }

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
open class MultiSecurityHttpConfig {
    @Order(Ordered.HIGHEST_PRECEDENCE)                                                      (1)
    @Bean
    open fun apiHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http {
            securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**"))           (2)
            authorizeExchange {
                authorize(anyExchange, authenticated)
            }
            oauth2ResourceServer {
                jwt { }                                                                     (3)
            }
        }
    }

    @Bean
    open fun webHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {            (4)
        return http {
            authorizeExchange {
                authorize(anyExchange, authenticated)
            }
            httpBasic { }                                                                   (5)
        }
    }

    @Bean
    open fun userDetailsService(): ReactiveUserDetailsService {
        return MapReactiveUserDetailsService(
            PasswordEncodedUser.user(), PasswordEncodedUser.admin()
        )
    }
}
1@Order を使用して SecurityWebFilterChain を構成し、Spring Security が最初に検討する必要がある SecurityWebFilterChain を指定します
2PathPatternParserServerWebExchangeMatcher を使用して、この SecurityWebFilterChain が /api/ で始まる URL パスにのみ適用されることを記述します
3/api/** エンドポイントに使用される認証メカニズムを指定します
4 他のすべての URL と一致するように、優先順位の低い SecurityWebFilterChain の別のインスタンスを作成します
5 アプリケーションの残りの部分で使用される認証メカニズムを指定します

Spring Security は、リクエストごとに 1 つの SecurityWebFilterChain @Bean を選択します。securityMatcher 定義の順番でリクエストに一致します。

この場合、URL パスが /api で始まる場合、Spring Security は apiHttpSecurity を使用することを意味します。URL が /api で始まらない場合、Spring Security はデフォルトで webHttpSecurity になります。これには、任意のリクエストに一致する暗黙の securityMatcher があります。

モジュラー ServerHttpSecurity 構成

多くのユーザーは、Spring Security の設定を一元管理することを好み、SecurityWebFilterChain Bean 宣言内で設定することを選択します。しかし、設定をモジュール化したい場合もあります。これは、以下の方法で実現できます。

カスタマイザー <ServerHttpSecurity> Bean

セキュリティ構成をモジュール化したい場合は、Customizer<ServerHttpSecurity> Bean にロジックを配置できます。例: 次の構成では、すべての ServerHttpSecurity インスタンスが次のように構成されます。

  • Java

  • Kotlin

@Bean
Customizer<ServerHttpSecurity> httpSecurityCustomizer() {
	return (http) -> http
		.headers((headers) -> headers
			.contentSecurityPolicy((csp) -> csp
				(1)
				.policyDirectives("object-src 'none'")
			)
		)
		(2)
		.redirectToHttps(Customizer.withDefaults());
}
@Bean
fun httpSecurityCustomizer(): Customizer<ServerHttpSecurity> {
    return Customizer { http -> http
        .headers { headers -> headers
            .contentSecurityPolicy { csp -> csp
                (1)
                .policyDirectives("object-src 'none'")
            }
        }
        (2)
        .redirectToHttps(Customizer.withDefaults())
    }
}

トップレベル ServerHttpSecurity カスタマイザー Bean

セキュリティ構成をさらにモジュール化したい場合は、Spring Security によってトップレベルの HttpSecurity および Customizer Bean が自動的に適用されます。

最上位の HttpSecurity 型または Customizer 型は、public HttpSecurity.*(Customizer<T>) に一致する任意の Customizer<T> として要約できます。これは、HttpSecurity (Javadoc) の public メソッドの単一引数である任意の Customizer<T> に相当します。

いくつかの例を挙げて説明しましょう。Customizer<ContentTypeOptionsConfig> が Bean として公開された場合、HeadersConfigurer.contentTypeOptions(Customizer) (Javadoc) の引数であり、HttpSecurity で定義されたメソッドではないため、自動的には適用されません。一方、Customizer<HeadersConfigurer<HttpSecurity>> が Bean として公開された場合、HttpSecurity.headers(Customizer) (Javadoc) の引数であるため、自動的に適用されます。

例: 次の構成では、コンテンツセキュリティポリシーが object-src 'none' に設定されます。

  • Java

  • Kotlin

@Bean
Customizer<ServerHttpSecurity.HeaderSpec> headersSecurity() {
	return (headers) -> headers
		.contentSecurityPolicy((csp) -> csp
			(1)
			.policyDirectives("object-src 'none'")
		);
}
@Bean
fun headersSecurity(): Customizer<ServerHttpSecurity.HeaderSpec> {
    return Customizer { headers -> headers
        .contentSecurityPolicy { csp -> csp
            (1)
            .policyDirectives("object-src 'none'")
        }
    }
}

カスタマイザー Bean オーダー

まず、各カスタマイザー <HttpSecurity> BeanObjectProvider#orderedStream() (Javadoc) を使用して適用されます。つまり、複数の Customizer<HttpSecurity> Bean がある場合、Bean 定義に @Order (Javadoc) アノテーションを追加して順序を制御できます。

次に、すべてのトップレベル HttpSecurity カスタマイザー Bean 型が検索され、それぞれが ObjectProvider#orderedStream() を使用して適用されます。Customizer<HeadersConfigurer<HttpSecurity>> Bean が 2 つと Customizer<HttpsRedirectConfigurer<HttpSecurity>> インスタンスが 2 つある場合、各 Customizer 型の呼び出し順序は未定義です。ただし、Customizer<HttpsRedirectConfigurer<HttpSecurity>> の各インスタンスの呼び出し順序は ObjectProvider#orderedStream() によって定義され、Bean 定義の @Order を使用して制御できます。

最後に、HttpSecurity Bean が Bean として注入されます。Customizer のすべてのインスタンスは、HttpSecurity Bean が作成される前に適用されます。これにより、Customizer Bean によって提供されるカスタマイズをオーバーライドできます。

順序を示す例を以下に示します。

  • Java

  • Kotlin

@Bean (4)
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
	http
		.authorizeExchange((exchange) -> exchange
			.anyExchange().authenticated()
		);
	return http.build();
}

@Bean
@Order(Ordered.LOWEST_PRECEDENCE) (2)
Customizer<ServerHttpSecurity> userAuthorization() {
	return (http) -> http
		.authorizeExchange((exchange) -> exchange
			.pathMatchers("/users/**").hasRole("USER")
		);
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) (1)
Customizer<ServerHttpSecurity> adminAuthorization() {
	return (http) -> http
		.authorizeExchange((exchange) -> exchange
			.pathMatchers("/admins/**").hasRole("ADMIN")
		);
}

(3)

@Bean
Customizer<ServerHttpSecurity.HeaderSpec> contentSecurityPolicy() {
	return (headers) -> headers
		.contentSecurityPolicy((csp) -> csp
			.policyDirectives("object-src 'none'")
		);
}

@Bean
Customizer<ServerHttpSecurity.HeaderSpec> contentTypeOptions() {
	return (headers) -> headers
		.contentTypeOptions(Customizer.withDefaults());
}

@Bean
Customizer<ServerHttpSecurity.HttpsRedirectSpec> httpsRedirect() {
	return Customizer.withDefaults();
}
@Bean (4)
fun springSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
    http
        .authorizeExchange({ exchanges -> exchanges
            .anyExchange().authenticated()
        })
    return http.build()
}

@Bean
@Order(Ordered.LOWEST_PRECEDENCE)  (2)
fun userAuthorization(): Customizer<ServerHttpSecurity> {
    return Customizer { http -> http
        .authorizeExchange { exchanges -> exchanges
            .pathMatchers("/users/**").hasRole("USER")
        }
    }
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) (1)
fun adminAuthorization(): Customizer<ServerHttpSecurity> {
    return ThrowingCustomizer { http -> http
        .authorizeExchange { exchanges -> exchanges
            .pathMatchers("/admins/**").hasRole("ADMIN")
        }
    }
}

(3)

@Bean
fun contentSecurityPolicy(): Customizer<ServerHttpSecurity.HeaderSpec> {
    return Customizer { headers -> headers
        .contentSecurityPolicy { csp -> csp
            .policyDirectives("object-src 'none'")
        }
    }
}

@Bean
fun contentTypeOptions(): Customizer<ServerHttpSecurity.HeaderSpec> {
    return Customizer { headers -> headers
        .contentTypeOptions(Customizer.withDefaults())
    }
}

@Bean
fun httpsRedirect(): Customizer<ServerHttpSecurity.HttpsRedirectSpec> {
    return Customizer.withDefaults()
}
1 まず、すべての Customizer<HttpSecurity> インスタンスが適用されます。adminAuthorization (Bean)は @Order が最も高いため、最初に適用されます。Customizer<HttpSecurity> Bean に @Order アノテーションがない場合、または @Order アノテーションの値が同じ場合、Customizer<HttpSecurity> インスタンスの適用順序は未定義です。
2userAuthorization は Customizer<HttpSecurity> のインスタンスであるため次に適用されます
3Customizer 型の順序は未定義です。この例では、contentSecurityPolicycontentTypeOptionshttpsRedirect の順序は未定義です。@Order(Ordered.HIGHEST_PRECEDENCE) が contentTypeOptions に追加された場合、contentTypeOptions が contentSecurityPolicy の前(同じ型)にあることはわかりますが、httpsRedirect が Customizer<HeadersConfigurer<HttpSecurity>> Bean の前か後かはわかりません。
4 すべての Customizer Bean が適用された後、HttpSecurity が Bean として渡されます。