RSocket セキュリティ
Spring Security の RSocket サポートは SocketAcceptorInterceptor
に依存しています。セキュリティへの主なエントリポイントは PayloadSocketAcceptorInterceptor
にあります。これは、RSocket API を適応させて、PayloadInterceptor
実装で PayloadExchange
をインターセプトできるようにします。
次の例は、最小限の RSocket セキュリティ構成を示しています。
Hello RSocket hellorsocket [GitHub] (英語)
最小限の 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)
}
}
}
RSocket 認証
RSocket 認証は、ReactiveAuthenticationManager
インスタンスを呼び出すコントローラーとして機能する AuthenticationPayloadInterceptor
を使用して実行されます。
セットアップ時の認証とリクエスト時間
通常、認証はセットアップ時またはリクエスト時、あるいはその両方で発生します。
セットアップ時の認証は、いくつかのシナリオで意味があります。一般的なシナリオは、単一のユーザー(モバイル接続など)が RSocket 接続を使用する場合です。この場合、接続を使用するのは 1 人のユーザーのみであるため、認証は接続時に 1 回実行できます。
RSocket 接続が共有されているシナリオでは、リクエストごとにクレデンシャルを送信するのが理にかなっています。例: ダウンストリームサービスとして RSocket サーバーに接続する Web アプリケーションは、すべてのユーザーが使用する単一の接続を確立します。この場合、RSocket サーバーが Web アプリケーションのユーザー資格情報に基づいて認可を実行する必要がある場合、各リクエストの認証は理にかなっています。
一部のシナリオでは、セットアップと各リクエストの両方での認証が理にかなっています。前に説明したように、Web アプリケーションについて考えてみます。Web アプリケーション自体への接続を制限する必要がある場合は、接続時に SETUP
権限を持つ資格情報を提供できます。その場合、各ユーザーは異なる権限を持つことができますが、SETUP
権限を持つことはできません。これは、個々のユーザーがリクエストを行うことはできますが、追加の接続を行うことはできないことを意味します。
単純認証
Spring Security はシンプル認証メタデータ拡張 [GitHub] (英語) をサポートしています。
基本認証は単純認証に進化し、下位互換性のためにのみサポートされています。設定については、 |
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 | このルールにより、まだルールがない交換は誰でも認可されます。この例では、メタデータを持たないペイロードにも認可ルールがないことを意味します。 |
認可ルールは順番に実行されることに注意してください。一致する最初の認可ルールのみが呼び出されます。