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

認可のカスタマイズ

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

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

カスタム MethodSecurityExpressionHandler
  • 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] (英語) を参照)。

カスタム認可マネージャー

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

メソッド前の認可は、メソッドが呼び出される前に実行されます。その認可がアクセスを拒否した場合、メソッドは呼び出されず、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 を使用して構築されていることに注意してください。インターセプターは指定された順序に基づいて呼び出されます。これは、次のようにインターセプターインスタンスで setOrder を呼び出すことによってカスタマイズできます。

カスタムアドバイザーを公開する
  • Java

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilterAuthorizationMethodInterceptor() {
	PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
	interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
	return interceptor;
}

アプリケーションで @PreAuthorize のみをサポートしたい場合は、次のようにすることができます。

@PreAuthorize 構成のみ
  • Java

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

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor preAuthorize() {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
	}
}

または、リストに追加するカスタムの before-method ReactiveAuthorizationManager がある場合があります。

この場合、ReactiveAuthorizationManager と、認可マネージャーが適用するメソッドとクラスの両方を Spring Security に通知する必要があります。

次のように、@PreAuthorize と @PostAuthorize の間で ReactiveAuthorizationManager を呼び出すように Spring Security を構成できます。

カスタムビフォアアドバイザー
  • Java

@EnableReactiveMethodSecurity(useAuthorizationManager=true)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public Advisor customAuthorize() {
		JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
		pattern.setPattern("org.mycompany.myapp.service.*");
		ReactiveAuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pattern, rule);
		interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
		return interceptor;
    }
}

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

メソッド後の認可についても同じことができます。メソッド後の認可は、通常、戻り値を分析してアクセスを検証することに関係しています。

例: 次のように、リクエストされたアカウントが実際にログインしたユーザーに属していることを確認するメソッドがある場合があります。

@PostAuthorize の例
  • Java

public interface BankService {

	@PreAuthorize("hasRole('USER')")
	@PostAuthorize("returnObject.owner == authentication.name")
	Mono<Account> readAccount(Long id);
}

独自の AuthorizationMethodInterceptor を指定して、戻り値へのアクセスの評価方法をカスタマイズできます。

例: 独自のカスタムアノテーションがある場合は、次のように構成できます。

カスタムアフターアドバイザー
  • Java

@EnableReactiveMethodSecurity(useAuthorizationManager=true)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public Advisor customAuthorize(ReactiveAuthorizationManager<MethodInvocationResult> rules) {
		AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(pattern, rules);
		interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
		return interceptor;
	}
}

@PostAuthorize インターセプターの後に呼び出されます。

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] (英語) ます。