最新の安定バージョンについては、Spring Security 6.3.1 を使用してください!

RSocket セキュリティ

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

以下のコードを示すサンプルアプリケーションをいくつか見つけることができます。

最小限の 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 インフラストラクチャを接続するものです。Spring Boot アプリケーションでは、これは次のコードで RSocketSecurityAutoConfiguration を使用して自動的に行われます。

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

RSocket 認証

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

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

一般に、認証はセットアップ時および / またはリクエスト時に発生する可能性があります。

セットアップ時の認証は、いくつかのシナリオで意味があります。一般的なシナリオは、1 人のユーザー(モバイル接続)が 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")
}

値は単なる文字列であるため、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 このルールはカスタム認証を活用します。マッチャーは、username という名前の変数を表現します。これは、context で使用可能になります。カスタム認証ルールは、checkFriends メソッドで公開されます。
5 このルールにより、まだルールを持たないリクエストでは、ユーザーの認証が必要になります。リクエストは、メタデータが含まれる場所です。追加のペイロードは含まれません。
6 このルールにより、ルールをまだ持っていない交換は誰でも認可されます。この例では、メタデータのないペイロードには認可ルールがないことを意味します。

認可ルールは順番に実行されることを理解することが重要です。一致する最初の認可ルールのみが呼び出されます。