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 實例。

設定時與請求時的身份驗證

一般而言,身份驗證可能在設定時或請求時或兩者都發生。

在某些情況下,設定時的身份驗證是有意義的。常見的情況是當單一使用者(例如行動連線)使用 RSocket 連線時。在這種情況下,只有單一使用者使用連線,因此身份驗證可以在連線時執行一次。

在 RSocket 連線共用的情況下,在每個請求上傳送憑證是有意義的。例如,作為下游服務連接到 RSocket 伺服器的網路應用程式將建立所有使用者都使用的單一連線。在這種情況下,如果 RSocket 伺服器需要根據網路應用程式的使用者憑證執行授權,則對每個請求進行身份驗證是有意義的。

在某些情況下,在設定時和每個請求時都進行身份驗證是有意義的。考慮先前描述的網路應用程式。如果我們需要將連線限制為網路應用程式本身,我們可以在連線時提供具有 SETUP 授權的憑證。然後,每個使用者可以具有不同的授權,但沒有 SETUP 授權。這表示個別使用者可以發出請求,但不能建立其他連線。

簡單身份驗證

Spring Security 支援 簡單身份驗證中繼資料擴充功能

基本身份驗證已演進為簡單身份驗證,僅為了向後相容性而支援。請參閱 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 權杖身份驗證中繼資料擴充功能。此支援的形式是驗證 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 的存在。以下可以找到從發行者建立一個範例

  • 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 此規則確保任何沒有規則的交換都允許任何人進行。在此範例中,這表示沒有中繼資料的酬載也沒有授權規則。

請注意,授權規則依序執行。僅調用第一個符合的授權規則。