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 基礎設施連線起來。

當您包含正確的依賴時,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 認證透過 `AuthenticationPayloadInterceptor` 執行,它充當控制器來呼叫 `ReactiveAuthenticationManager` 例項。

在 Setup 時刻還是 Request 時刻進行認證

通常,認證可以在建立連線時(setup time)發生,也可以在請求時(request time)發生,或者兩者都發生。

在一些場景下,在建立連線時進行認證是有意義的。一個常見的場景是當單個使用者(例如移動連線)使用 RSocket 連線時。在這種情況下,只有單個使用者使用該連線,因此可以在連線時進行一次認證。

在 RSocket 連線被共享的場景中,在每個請求上傳送憑證是有意義的。例如,一個作為下游服務連線到 RSocket 伺服器的 Web 應用會建立一個所有使用者共享的連線。在這種情況下,如果 RSocket 伺服器需要根據 Web 應用使用者的憑證執行授權,那麼對每個請求進行認證是有意義的。

在某些場景下,在建立連線時和每個請求時都進行認證是有意義的。考慮前面描述的 Web 應用。如果我們需要將連線限制到 Web 應用本身,我們可以在連線時提供具有 `SETUP` 許可權的憑證。然後每個使用者可以擁有不同的許可權,但不能擁有 `SETUP` 許可權。這意味著單個使用者可以發起請求,但不能建立額外的連線。

簡單認證

Spring Security 支援 Simple Authentication Metadata Extension

Basic Authentication 演變為 Simple Authentication,僅為向後相容而支援。請參閱 `RSocketSecurity.basicAuthentication(Customizer)` 進行設定。

RSocket 接收端可以透過使用 `AuthenticationPayloadExchangeConverter` 解碼憑證,該轉換器透過 DSL 的 `simpleAuthentication` 部分自動設定。以下示例展示了一個明確的配置

  • 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 傳送端可以透過使用 `SimpleAuthenticationEncoder` 傳送憑證,您可以將其新增到 Spring 的 `RSocketStrategies` 中。

  • 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 支援 Bearer Token Authentication Metadata Extension。這種支援形式為認證 JWT(確定 JWT 有效)然後使用 JWT 進行授權決策。

RSocket 接收端可以透過使用 `BearerPayloadExchangeConverter` 解碼憑證,該轉換器透過 DSL 的 `jwt` 部分自動設定。以下列表展示了一個示例配置

  • 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`。以下是根據頒發者建立該 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 傳送端傳送令牌不需要做任何特別的事情,因為該值是一個簡單的 `String`。以下示例在建立連線時傳送令牌

  • 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 授權透過 `AuthorizationPayloadInterceptor` 執行,它充當控制器來呼叫 `ReactiveAuthorizationManager` 例項。您可以使用 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 此規則確保任何尚未有規則的交換(exchange)都允許任何人訪問。在此示例中,這意味著沒有元資料的有效載荷也沒有授權規則。

請注意,授權規則按順序執行。只有第一個匹配的授權規則會被呼叫。