RSocket セキュリティ

Spring Security の RSocket サポートは SocketAcceptorInterceptor に依存しています。セキュリティへの主なエントリポイントは PayloadSocketAcceptorInterceptor にあります。これは、RSocket API を適応させて、PayloadInterceptor 実装で PayloadExchange をインターセプトできるようにします。

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

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

以下に、最小限の RSocket セキュリティ構成を示します。

  • Java

  • Kotlin

@Configuration
@EnableRSocketSecurity
public class HelloRSocketSecurityConfig {

	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
			.username("user")
			.password("user")
			.roles("USER")
			.build();
		return new MapReactiveUserDetailsService(user);
	}
}
@Configuration
@EnableRSocketSecurity
open class HelloRSocketSecurityConfig {
    @Bean
    open fun userDetailsService(): MapReactiveUserDetailsService {
        val user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("user")
            .roles("USER")
            .build()
        return MapReactiveUserDetailsService(user)
    }
}

この構成により、単純な認証が可能になり、rsocket-authorization がセットアップされて、リクエストに対して認証されたユーザーが必要になります。

SecuritySocketAcceptorInterceptor の追加

Spring Security が機能するには、SecuritySocketAcceptorInterceptor を ServerRSocketFactory に適用する必要があります。これにより、PayloadSocketAcceptorInterceptor が RSocket インフラストラクチャに接続されます。

正しい依存関係 [GitHub] (英語) を含めると、Spring Boot はそれを RSocketSecurityAutoConfiguration に自動的に登録します。

または、Boot の自動構成を使用していない場合は、次の方法で手動で登録できます。

  • Java

  • Kotlin

@Bean
RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) {
    return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor));
}
@Bean
fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor): RSocketServerCustomizer {
    return RSocketServerCustomizer { server ->
        server.interceptors { registry ->
            registry.forSocketAcceptor(interceptor)
        }
    }
}

インターセプター自体をカスタマイズするには、RSocketSecurity を使用して認証認可を追加します。

RSocket 認証

RSocket 認証は、ReactiveAuthenticationManager インスタンスを呼び出すコントローラーとして機能する AuthenticationPayloadInterceptor を使用して実行されます。

セットアップ時の認証とリクエスト時間

通常、認証はセットアップ時またはリクエスト時、あるいはその両方で発生します。

セットアップ時の認証は、いくつかのシナリオで意味があります。一般的なシナリオは、単一のユーザー(モバイル接続など)が RSocket 接続を使用する場合です。この場合、接続を使用するのは 1 人のユーザーのみであるため、認証は接続時に 1 回実行できます。

RSocket 接続が共有されているシナリオでは、リクエストごとにクレデンシャルを送信するのが理にかなっています。例: ダウンストリームサービスとして RSocket サーバーに接続する Web アプリケーションは、すべてのユーザーが使用する単一の接続を確立します。この場合、RSocket サーバーが Web アプリケーションのユーザー資格情報に基づいて認可を実行する必要がある場合、各リクエストの認証は理にかなっています。

一部のシナリオでは、セットアップと各リクエストの両方での認証が理にかなっています。前に説明したように、Web アプリケーションについて考えてみます。Web アプリケーション自体への接続を制限する必要がある場合は、接続時に SETUP 権限を持つ資格情報を提供できます。その場合、各ユーザーは異なる権限を持つことができますが、SETUP 権限を持つことはできません。これは、個々のユーザーがリクエストを行うことはできますが、追加の接続を行うことはできないことを意味します。

単純認証

Spring Security はシンプル認証メタデータ拡張 [GitHub] (英語) をサポートしています。

基本認証は単純認証に進化し、下位互換性のためにのみサポートされています。設定については、RSocketSecurity.basicAuthentication(Customizer) を参照してください。

RSocket レシーバーは、DSL の simpleAuthentication 部分を使用して自動的にセットアップされる AuthenticationPayloadExchangeConverter を使用して資格情報をデコードできます。次の例は、明示的な構成を示しています。

  • Java

  • Kotlin

@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
	rsocket
		.authorizePayload(authorize ->
			authorize
					.anyRequest().authenticated()
					.anyExchange().permitAll()
		)
		.simpleAuthentication(Customizer.withDefaults());
	return rsocket.build();
}
@Bean
open fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
    rsocket
        .authorizePayload { authorize -> authorize
                .anyRequest().authenticated()
                .anyExchange().permitAll()
        }
        .simpleAuthentication(withDefaults())
    return rsocket.build()
}

RSocket 送信者は、Spring の RSocketStrategies に追加できる SimpleAuthenticationEncoder を使用して資格情報を送信できます。

  • Java

  • Kotlin

RSocketStrategies.Builder strategies = ...;
strategies.encoder(new SimpleAuthenticationEncoder());
var strategies: RSocketStrategies.Builder = ...
strategies.encoder(SimpleAuthenticationEncoder())

次に、それを使用して、セットアップの受信者にユーザー名とパスワードを送信できます。

  • Java

  • Kotlin

MimeType authenticationMimeType =
	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
Mono<RSocketRequester> requester = RSocketRequester.builder()
	.setupMetadata(credentials, authenticationMimeType)
	.rsocketStrategies(strategies.build())
	.connectTcp(host, port);
val authenticationMimeType: MimeType =
    MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
val credentials = UsernamePasswordMetadata("user", "password")
val requester: Mono<RSocketRequester> = RSocketRequester.builder()
    .setupMetadata(credentials, authenticationMimeType)
    .rsocketStrategies(strategies.build())
    .connectTcp(host, port)

代替的または追加的に、リクエストでユーザー名とパスワードを送信できます。

  • Java

  • Kotlin

Mono<RSocketRequester> requester;
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");

public Mono<AirportLocation> findRadar(String code) {
	return this.requester.flatMap(req ->
		req.route("find.radar.{code}", code)
			.metadata(credentials, authenticationMimeType)
			.retrieveMono(AirportLocation.class)
	);
}
import org.springframework.messaging.rsocket.retrieveMono

// ...

var requester: Mono<RSocketRequester>? = null
var credentials = UsernamePasswordMetadata("user", "password")

open fun findRadar(code: String): Mono<AirportLocation> {
    return requester!!.flatMap { req ->
        req.route("find.radar.{code}", code)
            .metadata(credentials, authenticationMimeType)
            .retrieveMono<AirportLocation>()
    }
}

JWT

Spring Security はベアラートークン認証メタデータ拡張 [GitHub] (英語) をサポートしています。このサポートは、JWT を認証し(JWT が有効であると判断する)、JWT を使用して認可を決定するという形で提供されます。

RSocket レシーバーは、DSL の jwt 部分を使用して自動的にセットアップされる BearerPayloadExchangeConverter を使用して資格情報をデコードできます。次のリストは、構成例を示しています。

  • Java

  • Kotlin

@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
	rsocket
		.authorizePayload(authorize ->
			authorize
				.anyRequest().authenticated()
				.anyExchange().permitAll()
		)
		.jwt(Customizer.withDefaults());
	return rsocket.build();
}
@Bean
fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
    rsocket
        .authorizePayload { authorize -> authorize
            .anyRequest().authenticated()
            .anyExchange().permitAll()
        }
        .jwt(withDefaults())
    return rsocket.build()
}

上記の構成は、ReactiveJwtDecoder@Bean が存在することに依存しています。発行者から作成する例を以下に示します。

  • Java

  • Kotlin

@Bean
ReactiveJwtDecoder jwtDecoder() {
	return ReactiveJwtDecoders
		.fromIssuerLocation("https://example.com/auth/realms/demo");
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return ReactiveJwtDecoders
        .fromIssuerLocation("https://example.com/auth/realms/demo")
}

値は単純な String であるため、RSocket 送信者はトークンを送信するために特別なことをする必要はありません。次の例では、セットアップ時にトークンを送信します。

  • Java

  • Kotlin

MimeType authenticationMimeType =
	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
BearerTokenMetadata token = ...;
Mono<RSocketRequester> requester = RSocketRequester.builder()
	.setupMetadata(token, authenticationMimeType)
	.connectTcp(host, port);
val authenticationMimeType: MimeType =
    MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
val token: BearerTokenMetadata = ...

val requester = RSocketRequester.builder()
    .setupMetadata(token, authenticationMimeType)
    .connectTcp(host, port)

代わりに、または追加で、リクエストでトークンを送信できます。

  • Java

  • Kotlin

MimeType authenticationMimeType =
	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
Mono<RSocketRequester> requester;
BearerTokenMetadata token = ...;

public Mono<AirportLocation> findRadar(String code) {
	return this.requester.flatMap(req ->
		req.route("find.radar.{code}", code)
	        .metadata(token, authenticationMimeType)
			.retrieveMono(AirportLocation.class)
	);
}
val authenticationMimeType: MimeType =
    MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
var requester: Mono<RSocketRequester>? = null
val token: BearerTokenMetadata = ...

open fun findRadar(code: String): Mono<AirportLocation> {
    return this.requester!!.flatMap { req ->
        req.route("find.radar.{code}", code)
            .metadata(token, authenticationMimeType)
            .retrieveMono<AirportLocation>()
    }
}

RSocket 認証

RSocket 認可は、ReactiveAuthorizationManager インスタンスを呼び出すためのコントローラーとして機能する AuthorizationPayloadInterceptor を使用して実行されます。DSL を使用して、PayloadExchange に基づく認可ルールを設定できます。次のリストは、構成例を示しています。

  • Java

  • Kotlin

rsocket
	.authorizePayload(authz ->
		authz
			.setup().hasRole("SETUP") (1)
			.route("fetch.profile.me").authenticated() (2)
			.matcher(payloadExchange -> isMatch(payloadExchange)) (3)
				.hasRole("CUSTOM")
			.route("fetch.profile.{username}") (4)
				.access((authentication, context) -> checkFriends(authentication, context))
			.anyRequest().authenticated() (5)
			.anyExchange().permitAll() (6)
	);
rsocket
    .authorizePayload { authz ->
        authz
            .setup().hasRole("SETUP") (1)
            .route("fetch.profile.me").authenticated() (2)
            .matcher { payloadExchange -> isMatch(payloadExchange) } (3)
            .hasRole("CUSTOM")
            .route("fetch.profile.{username}") (4)
            .access { authentication, context -> checkFriends(authentication, context) }
            .anyRequest().authenticated() (5)
            .anyExchange().permitAll()
    } (6)
1 接続をセットアップするには、ROLE_SETUP 権限が必要です。
2 ルートが fetch.profile.me の場合、認可にはユーザーの認証のみが必要です。
3 このルールでは、カスタムマッチャーを設定します。この場合、認可にはユーザーに ROLE_CUSTOM 権限が必要です。
4 このルールはカスタム認可を使用します。マッチャーは、context で使用可能になる username という名前の変数を表現します。カスタム認可ルールは、checkFriends メソッドで公開されます。
5 このルールにより、まだルールがないリクエストでは、ユーザーの認証が必要になります。リクエストは、メタデータが含まれる場所です。追加のペイロードは含まれません。
6 このルールにより、まだルールがない交換は誰でも認可されます。この例では、メタデータを持たないペイロードにも認可ルールがないことを意味します。

認可ルールは順番に実行されることに注意してください。一致する最初の認可ルールのみが呼び出されます。