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)都允许任何人访问。在此示例中,这意味着没有元数据的有效载荷也没有授权规则。

请注意,授权规则按顺序执行。只有第一个匹配的授权规则会被调用。