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 服务器的 Web 应用程序将建立所有用户都使用的单个连接。在这种情况下,如果 RSocket 服务器需要根据 Web 应用程序用户的凭证执行授权,则对每个请求进行身份验证是有意义的。

在某些情况下,在设置和每个请求中进行身份验证是有意义的。考虑如前所述的 Web 应用程序。如果我们需要将连接限制到 Web 应用程序本身,我们可以在连接时使用具有 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。以下是可以从颁发者处创建该 @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 此规则确保尚未有规则的任何交换对任何人都是允许的。在此示例中,这意味着没有元数据的有效负载也没有授权规则。

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