# RSocket 安全性

Spring Security 的 RSocket 支持依赖于SocketAcceptorInterceptor。进入安全的主要入口点是PayloadSocketAcceptorInterceptor,它调整了 RSocket API,允许使用PayloadInterceptor实现来拦截PayloadExchange

你可以找到几个示例应用程序来演示下面的代码:

# 最小的 RSocket 安全配置

你可以在下面找到最小的 RSocket 安全配置:

爪哇

@Configuration
@EnableRSocketSecurity
public class HelloRSocketSecurityConfig {

	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
			.username("user")
			.password("user")
			.roles("USER")
			.build();
		return new MapReactiveUserDetailsService(user);
	}
}

Kotlin

@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-授权以要求对任何请求进行身份验证的用户。

# 添加安全性:接收截取程序

为了使安全性工作,我们需要将SecuritySocketAcceptorInterceptor应用到ServerRSocketFactory。这就是将我们创建的PayloadSocketAcceptorInterceptor与 RSocket 基础架构连接起来的原因。在 Spring 引导应用程序中,这是使用RSocketSecurityAutoConfiguration和以下代码自动完成的。

@Bean
RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) {
    return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor));
}

# RSocket 身份验证

RSocket 身份验证是通过AuthenticationPayloadInterceptor执行的,该控制器充当调用ReactiveAuthenticationManager实例的控制器。

# 设置时的身份验证与请求时的身份验证

通常,身份验证可以在设置时间和/或请求时间发生。

在几个场景中,设置时的身份验证是有意义的。一个常见的场景是当单个用户(即移动连接)利用 RSocket 连接时。在这种情况下,只有单个用户在利用连接,因此可以在连接时进行一次身份验证。

在共享 RSocket 连接的场景中,对每个请求发送凭据是有意义的。例如,作为下游服务连接到 RSocket 服务器的 Web 应用程序将生成一个所有用户都可以利用的单个连接。在这种情况下,如果 RSocket 服务器需要根据每个请求的 Web 应用程序的用户凭据来执行授权,这是有意义的。

在某些情况下,在设置和每个请求中进行身份验证是有意义的。考虑如前所述的 Web 应用程序。如果我们需要将连接限制到 Web 应用程序本身,那么我们可以在连接时提供一个具有SETUP权限的凭据。然后,每个用户将拥有不同的权限,但不是SETUP权限。这意味着个人用户可以提出请求,但不能建立额外的连接。

# 简单的身份验证

Spring 安全性支持简单的身份验证元数据扩展 (opens new window)

基本的身份验证草案演变成简单的身份验证,并且只支持向后兼容性。
设置它的方法请参见RSocketSecurity.basicAuthentication(Customizer)

RSocket 接收器可以使用AuthenticationPayloadExchangeConverter来解码凭据,这是使用 DSL 的simpleAuthentication部分自动设置的。可以在下面找到一个显式配置。

爪哇

@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
	rsocket
		.authorizePayload(authorize ->
			authorize
					.anyRequest().authenticated()
					.anyExchange().permitAll()
		)
		.simpleAuthentication(Customizer.withDefaults());
	return rsocket.build();
}

Kotlin

@Bean
open fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
    rsocket
        .authorizePayload { authorize -> authorize
                .anyRequest().authenticated()
                .anyExchange().permitAll()
        }
        .simpleAuthentication(withDefaults())
    return rsocket.build()
}

RSocket 发送者可以使用SimpleAuthenticationEncoder发送凭据,该凭据可以添加到 Spring 的RSocketStrategies中。

爪哇

RSocketStrategies.Builder strategies = ...;
strategies.encoder(new SimpleAuthenticationEncoder());

Kotlin

var strategies: RSocketStrategies.Builder = ...
strategies.encoder(SimpleAuthenticationEncoder())

然后可以使用它向设置中的接收器发送用户名和密码:

爪哇

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);

Kotlin

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)

另外,也可以在请求中发送用户名和密码。

爪哇

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)
	);
}

Kotlin

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 安全性支持承载令牌身份验证元数据扩展 (opens new window)。支持的形式是对 JWT 进行身份验证(确定 JWT 是有效的),然后使用 JWT 做出授权决策。

RSocket 接收器可以使用BearerPayloadExchangeConverter来解码凭据,这是使用 DSL 的jwt部分自动设置的。下面是一个配置示例:

爪哇

@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
	rsocket
		.authorizePayload(authorize ->
			authorize
				.anyRequest().authenticated()
				.anyExchange().permitAll()
		)
		.jwt(Customizer.withDefaults());
	return rsocket.build();
}

Kotlin

@Bean
fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
    rsocket
        .authorizePayload { authorize -> authorize
            .anyRequest().authenticated()
            .anyExchange().permitAll()
        }
        .jwt(withDefaults())
    return rsocket.build()
}

上述配置依赖于存在ReactiveJwtDecoder``@Bean。可以在下面找到从发行者创建一个发行者的示例:

爪哇

@Bean
ReactiveJwtDecoder jwtDecoder() {
	return ReactiveJwtDecoders
		.fromIssuerLocation("https://example.com/auth/realms/demo");
}

Kotlin

@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
    return ReactiveJwtDecoders
        .fromIssuerLocation("https://example.com/auth/realms/demo")
}

RSocket 发送者不需要做任何特殊的事情来发送令牌,因为该值只是一个简单的字符串。例如,可以在设置时发送令牌:

爪哇

MimeType authenticationMimeType =
	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
BearerTokenMetadata token = ...;
Mono<RSocketRequester> requester = RSocketRequester.builder()
	.setupMetadata(token, authenticationMimeType)
	.connectTcp(host, port);

Kotlin

val authenticationMimeType: MimeType =
    MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
val token: BearerTokenMetadata = ...

val requester = RSocketRequester.builder()
    .setupMetadata(token, authenticationMimeType)
    .connectTcp(host, port)

可选地或附加地,令牌可以在请求中发送。

爪哇

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)
	);
}

Kotlin

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设置授权规则。下面是一个配置示例:

爪哇

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)
	);

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)
1 建立连接需要授权ROLE_SETUP
2 如果路由是fetch.profile.me,则授权只需要对用户进行身份验证即可。
3 在这个规则中,我们设置了一个自定义的匹配器,其中授权要求用户拥有ROLE_CUSTOM的权限
4 该规则利用自定义授权。
Matcher 表示一个名为username的变量,该变量在context中可用。
checkFriends方法中公开了一个自定义授权规则。
5 此规则确保尚未具有规则的请求将需要对用户进行身份验证。
包含元数据的位置是一个请求。
它将不包括额外的有效负载。
6 此规则确保任何人都可以使用任何尚未拥有规则的交换。
在本例中,这意味着没有元数据的有效负载没有授权规则。

重要的是要理解授权规则是按顺序执行的。将只调用匹配的第一个授权规则。

CORSTesting