EnableReactiveMethodSecurity

Spring Security は、ReactiveSecurityContextHolder によって設定された Reactor のコンテキスト (英語) を使用して、メソッドのセキュリティをサポートします。次の例は、現在ログインしているユーザーのメッセージを取得する方法を示しています。

この例が機能するには、メソッドの戻り値の型が org.reactivestreams.Publisher (つまり、Mono または Flux) である必要があります。これは、Reactor の Context と統合するために必要です。

EnableReactiveMethodSecurity と AuthorizationManager

Spring Security 5.8 では、任意の @Configuration インスタンスで @EnableReactiveMethodSecurity(useAuthorizationManager=true) アノテーションを使用して、アノテーションベースのセキュリティを有効にできます。

これは、いくつかの点で @EnableReactiveMethodSecurity を改善します。@EnableReactiveMethodSecurity(useAuthorizationManager=true):

  1. メタデータソース、構成属性、意思決定マネージャー、投票者の代わりに、簡略化された AuthorizationManager API を使用します。これにより、再利用とカスタマイズが簡単になります。

  2. Kotlin コルーチンを含むリアクティブ戻り値型をサポートします。

  3. ネイティブ Spring AOP を使用して構築され、抽象化を削除し、Spring AOP ビルドブロックを使用してカスタマイズできるようにします

  4. 競合するアノテーションをチェックして、明確なセキュリティ構成を確保します

  5. JSR-250 に準拠

以前のバージョンについては、@EnableReactiveMethodSecurity での同様のサポートについて参照してください。

例: 次の場合、Spring Security の @PreAuthorize アノテーションが有効になります。

メソッドセキュリティ構成
  • Java

@EnableReactiveMethodSecurity(useAuthorizationManager=true)
public class MethodSecurityConfig {
	// ...
}

(クラスまたはインターフェースの) メソッドにアノテーションを追加すると、それに応じてそのメソッドへのアクセスが制限されます。Spring Security のネイティブアノテーションサポートは、メソッドの一連の属性を定義します。これらは、実際の決定を行うために、AuthorizationManagerBeforeReactiveMethodInterceptor などのさまざまなメソッドインターセプターに渡されます。

メソッドセキュリティアノテーションの使用箇所
  • Java

public interface BankService {
	@PreAuthorize("hasRole('USER')")
	Mono<Account> readAccount(Long id);

	@PreAuthorize("hasRole('USER')")
	Flux<Account> findAccounts();

	@PreAuthorize("@func.apply(#account)")
	Mono<Account> post(Account account, Double amount);
}

この場合、hasRole は、SpEL 評価エンジンによって呼び出される SecurityExpressionRoot にあるメソッドを参照します。

@bean は、ユーザーが定義したカスタムコンポーネントを参照します。apply は、認可の決定を示すために Boolean または Mono<Boolean> を返すことができます。そのような Bean は次のようになります。

メソッドセキュリティリアクティブブール式
  • Java

@Bean
public Function<Account, Mono<Boolean>> func() {
    return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
}

メソッド認証は、メソッド前とメソッド後の認証を組み合わせたものです。

メソッド前の認可は、メソッドが呼び出される前に実行されます。その認可がアクセスを拒否した場合、メソッドは呼び出されず、AccessDeniedException がスローされます。メソッド後の認可は、メソッドが呼び出された後、メソッドが呼び出し元に戻る前に実行されます。その認可がアクセスを拒否した場合、値は返されず、AccessDeniedException がスローされます

@EnableReactiveMethodSecurity(useAuthorizationManager=true) の追加がデフォルトで行うことを再現するには、次の構成を公開します。

完全なポストポストメソッドのセキュリティ構成
  • Java

@Configuration
class MethodSecurityConfig {
	@Bean
	BeanDefinitionRegistryPostProcessor aopConfig() {
		return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor() {
		return new PreFilterAuthorizationReactiveMethodInterceptor();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor() {
		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor() {
		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor() {
		return new PostFilterAuthorizationReactiveMethodInterceptor();
	}
}

Spring Security のメソッドセキュリティは Spring AOP を使用して構築されていることに注意してください。

認可のカスタマイズ

Spring Security の @PreAuthorize@PostAuthorize@PreFilter@PostFilter には、表現ベースの豊富なサポートが付属しています。

また、ロールベースの認可の場合、Spring Security はデフォルトの ROLE_ プレフィックスを追加します。これは、hasRole などの式を評価するときに使用されます。次のように、GrantedAuthorityDefaults Bean を公開することで、別のプレフィックスを使用するように認可規則を構成できます。

カスタム GrantedAuthorityDefaults
  • Java

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
	return new GrantedAuthorityDefaults("MYPREFIX_");
}

static メソッドを使用して GrantedAuthorityDefaults を公開し、Spring が Spring Security のメソッドセキュリティ @Configuration クラスを初期化する前にそれを公開するようにします。GrantedAuthorityDefaults Bean は Spring Security の内部動作の一部であるため、Bean 後処理に関連するいくつかの警告を効果的に回避するために、インフラストラクチャ Bean としても公開する必要があります ( gh-14751 [GitHub] (英語) を参照)。

プログラムによるメソッドの承認

すでに見たように、メソッドセキュリティ SpEL 式を使用して重要な認可ルールを指定できる方法はいくつかあります。

ロジックを SpEL ベースではなく Java ベースにする方法はいくつかあります。これにより、Java 言語全体にアクセスできるようになり、テスト容易性とフロー制御が向上します。

SpEL でのカスタム Bean の使用

メソッドをプログラム的に認証する最初の方法は、2 段階のプロセスです。

まず、次のように MethodSecurityExpressionOperations インスタンスを受け取るメソッドを持つ Bean を宣言します。

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public decide(MethodSecurityExpressionOperations operations): Mono<Boolean> {
        // ... authorization logic
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): Mono<Boolean> {
        // ... authorization logic
    }
}

次に、次の方法でアノテーション内でその Bean を参照します。

  • Java

  • Kotlin

@Controller
public class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    public Mono<String> endpoint() {
        // ...
    }
}
@Controller
open class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    fun endpoint(): Mono<String> {
        // ...
    }
}

Spring Security は、メソッド呼び出しごとに Bean 上で指定されたメソッドを呼び出します。

これの優れた点は、すべての認可ロジックが別個のクラスにあり、個別に単体テストを行って正確性を検証できることです。完全な Java 言語にもアクセスできます。

Mono<Boolean> を返すだけでなく、コードが決定を控えていることを示すために Mono.empty() を返すこともできます。

決定の性質に関する詳細情報を含める場合は、代わりに次のようなカスタム AuthorizationDecision を返すことができます。

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public Mono<AuthorizationDecision> decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
        return Mono.just(new MyAuthorizationDecision(false, details));
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): Mono<AuthorizationDecision> {
        // ... authorization logic
        return Mono.just(MyAuthorizationDecision(false, details))
    }
}

または、カスタム AuthorizationDeniedException インスタンスをスローします。ただし、スタックトレースを生成するコストが発生しないため、オブジェクトを返すことが推奨されることに注意してください。

その後、認可結果の処理方法をカスタマイズするときに、カスタム詳細にアクセスできます。

カスタム認証マネージャーの使用

メソッドをプログラム的に承認する 2 番目の方法は、カスタム AuthorizationManager を作成することです。

まず、次のように認可マネージャーインスタンスを宣言します。

  • Java

  • Kotlin

@Component
public class MyPreAuthorizeAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
    @Override
    public Mono<AuthorizationDecision> check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // ... authorization logic
    }

}
@Component
class MyPreAuthorizeAuthorizationManager : ReactiveAuthorizationManager<MethodInvocation> {
    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): Mono<AuthorizationDecision> {
        // ... authorization logic
    }

}

次に、ReactiveAuthorizationManager を実行するタイミングに対応するポイントカットを使用してメソッドインターセプターを公開します。例: @PreAuthorize と @PostAuthorize の動作を次のように置き換えることができます。

@PreAuthorize および @PostAuthorize 構成のみ
  • Java

  • Kotlin

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) {
		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager);
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) {
		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager);
	}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
   	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun preAuthorize(val manager: MyPreAuthorizeAuthorizationManager) : Advisor {
		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager)
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize(val manager: MyPostAuthorizeAuthorizationManager) : Advisor {
		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager)
	}
}

AuthorizationInterceptorsOrder で指定された順序定数を使用して、Spring Security メソッドインターセプターの間にインターセプターを配置できます。

式処理のカスタマイズ

または、3 番目に、各 SpEL 式の処理方法をカスタマイズできます。これを行うには、次のようにカスタム MethodSecurityExpressionHandler を公開します。

カスタム MethodSecurityExpressionHandler
  • Java

  • Kotlin

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
	DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
	handler.setRoleHierarchy(roleHierarchy);
	return handler;
}
companion object {
	@Bean
	fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
		val handler = DefaultMethodSecurityExpressionHandler()
		handler.setRoleHierarchy(roleHierarchy)
		return handler
	}
}

static メソッドを使用して MethodSecurityExpressionHandler を公開し、Spring Security のメソッドセキュリティ @Configuration クラスを初期化する前に Spring がそれを公開するようにします。

DefaultMessageSecurityExpressionHandler をサブクラス化して、デフォルト以外の独自のカスタム認証式を追加することもできます。

EnableReactiveMethodSecurity

  • Java

  • Kotlin

Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");

Mono<String> messageByUsername = ReactiveSecurityContextHolder.getContext()
	.map(SecurityContext::getAuthentication)
	.map(Authentication::getName)
	.flatMap(this::findMessageByUsername)
	// In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
	.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));

StepVerifier.create(messageByUsername)
	.expectNext("Hi user")
	.verifyComplete();
val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER")

val messageByUsername: Mono<String> = ReactiveSecurityContextHolder.getContext()
	.map(SecurityContext::getAuthentication)
	.map(Authentication::getName)
	.flatMap(this::findMessageByUsername) // In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
	.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))

StepVerifier.create(messageByUsername)
	.expectNext("Hi user")
	.verifyComplete()

this::findMessageByUsername は次のように定義されます。

  • Java

  • Kotlin

Mono<String> findMessageByUsername(String username) {
	return Mono.just("Hi " + username);
}
fun findMessageByUsername(username: String): Mono<String> {
	return Mono.just("Hi $username")
}

次の最小限のメソッドセキュリティは、リアクティブアプリケーションのメソッドセキュリティを構成します。

  • Java

  • Kotlin

@Configuration
@EnableReactiveMethodSecurity
public class SecurityConfig {
	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
		UserDetails rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build();
		UserDetails admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER","ADMIN")
			.build();
		return new MapReactiveUserDetailsService(rob, admin);
	}
}
@Configuration
@EnableReactiveMethodSecurity
class SecurityConfig {
	@Bean
	fun userDetailsService(): MapReactiveUserDetailsService {
		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
		val rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build()
		val admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER", "ADMIN")
			.build()
		return MapReactiveUserDetailsService(rob, admin)
	}
}

次のクラスを検討してください。

  • Java

  • Kotlin

@Component
public class HelloWorldMessageService {
	@PreAuthorize("hasRole('ADMIN')")
	public Mono<String> findMessage() {
		return Mono.just("Hello World!");
	}
}
@Component
class HelloWorldMessageService {
	@PreAuthorize("hasRole('ADMIN')")
	fun findMessage(): Mono<String> {
		return Mono.just("Hello World!")
	}
}

または、次のクラスは Kotlin コルーチンを使用します。

  • Kotlin

@Component
class HelloWorldMessageService {
    @PreAuthorize("hasRole('ADMIN')")
    suspend fun findMessage(): String {
        delay(10)
        return "Hello World!"
    }
}

上記の構成と組み合わせると、@PreAuthorize("hasRole('ADMIN')") は、findByMessage が ADMIN ロールを持つユーザーによってのみ呼び出されることを保証します。標準メソッドセキュリティの式はいずれも @EnableReactiveMethodSecurity で機能することに注意してください。ただし、現時点では、式の Boolean または boolean の戻り型のみをサポートしています。これは、式がブロックしてはならないことを意味します。

WebFlux セキュリティと統合する場合、Reactor コンテキストは、認証されたユーザーに応じて Spring Security によって自動的に確立されます。

  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {

	@Bean
	SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
		return http
			// Demonstrate that method security works
			// Best practice to use both for defense in depth
			.authorizeExchange(exchanges -> exchanges
				.anyExchange().permitAll()
			)
			.httpBasic(withDefaults())
			.build();
	}

	@Bean
	MapReactiveUserDetailsService userDetailsService() {
		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
		UserDetails rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build();
		UserDetails admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER","ADMIN")
			.build();
		return new MapReactiveUserDetailsService(rob, admin);
	}
}
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class SecurityConfig {
	@Bean
	open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			authorizeExchange {
				authorize(anyExchange, permitAll)
			}
			httpBasic { }
		}
	}

	@Bean
	fun userDetailsService(): MapReactiveUserDetailsService {
		val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
		val rob = userBuilder.username("rob")
			.password("rob")
			.roles("USER")
			.build()
		val admin = userBuilder.username("admin")
			.password("admin")
			.roles("USER", "ADMIN")
			.build()
		return MapReactiveUserDetailsService(rob, admin)
	}
}

完全なサンプルは hellowebflux-method にあり [GitHub] (英語) ます。