RSocket 安全

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

Spring Security的RSocket支持依赖于 SocketAcceptorInterceptor。security 的主要入口在 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)
    }
}

这种配置可以实现 simple authentication,并设置 rsocket-authorization,要求任何请求都要有认证的用户。

添加 SecuritySocketAcceptorInterceptor

为了让Spring Security发挥作用,我们需要将 SecuritySocketAcceptorInterceptor 应用于 ServerRSocketFactory。这样做可以将我们的 PayloadSocketAcceptorInterceptor 与 RSocket 基础设施连接起来。在Spring Boot应用程序中,你可以通过使用 RSocketSecurityAutoConfiguration 来自动完成这一工作,代码如下。

  • 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认证是通过 AuthenticationPayloadInterceptor 进行的,它作为一个控制器来调用 ReactiveAuthenticationManager 实例。

设置时的认证与请求时的认证

一般来说,认证可以在设置时或在请求时进行,也可以同时进行。

在设置时进行认证在一些情况下是有意义的。一个常见的情况是单个用户(如移动连接)使用一个RSocket连接。在这种情况下,只有一个用户使用该连接,所以认证可以在连接时进行一次。

在共享RSocket连接的情况下,在每个请求中发送凭证是有意义的。例如,作为下游服务连接到RSocket服务器的Web应用程序将建立一个所有用户都使用的单一连接。在这种情况下,如果RSocket服务器需要根据网络应用程序的用户凭证执行授权,对每个请求进行认证是有意义的。

在某些情况下,在设置和每个请求中都进行认证是有意义的。考虑一个web应用,如前所述。如果我们需要限制连接到网络应用程序本身,我们可以在连接时提供一个具有 SETUP 权限的凭证。然后每个用户可以有不同的权限,但不能有 SETUP 权限。这意味着单个用户可以发出请求,但不能进行额外的连接。

Simple Authentication(简易认证)

Spring Security 支持 Simple Authentication Metadata Extension

基本认证(Basic Authentication)演变为简单认证(Simple Authentication),只支持向后兼容。请参阅 RSocketSecurity.basicAuthentication(Customizer) 来设置它。

RSocket receiver 可以通过使用 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 认证元数据扩展。这种支持是以验证JWT(确定JWT有效)的形式出现的,然后使用JWT来做出授权决定。

RSocket receiver 可以通过使用 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 的存在。下面是一个从 issuer 创建一个的例子。

  • 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 进行的,它作为一个 controller 来调用 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 这个规则确保尚未有规则的请求需要用户进行认证。一个请求是包括元数据的地方。它不会包括额外的payload。
6 这个规则确保任何尚未有规则的 exchange 都是允许任何人使用的。在这个例子中,它意味着没有元数据的payload也没有授权规则。

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