RSocket Security

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

Spring Security’s RSocket support relies on a SocketAcceptorInterceptor. The main entry point into security is in PayloadSocketAcceptorInterceptor, which adapts the RSocket APIs to allow intercepting a PayloadExchange with PayloadInterceptor implementations.

以下示例显示了极简的 RSocket 安全配置:

The following example shows a minimal RSocket Security configuration:

Minimal RSocket Security Configuration

以下为极简 RSocket 安全配置:

You can find a minimal RSocket Security configuration below:

  • 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 以要求经过认证的用户进行任何请求。

This configuration enables rsocket-authentication-simple and sets up rsocket-authorization to require an authenticated user for any request.

Adding SecuritySocketAcceptorInterceptor

对于 Spring Security 来说,我们需将 SecuritySocketAcceptorInterceptor 应用到 ServerRSocketFactory。这样做会将我们的 PayloadSocketAcceptorInterceptor 与 RSocket 基础设施连接。

For Spring Security to work, we need to apply SecuritySocketAcceptorInterceptor to the ServerRSocketFactory. Doing so connects our PayloadSocketAcceptorInterceptor with the RSocket infrastructure.

在您包含https://github.com/spring-projects/spring-security-samples/tree/{gh-tag}/reactive/rsocket/hello-security/build.gradle[正确的依赖关系]时,Spring Boot会自动在`RSocketSecurityAutoConfiguration`中注册它。

Spring Boot registers it automatically in RSocketSecurityAutoConfiguration when you include the correct dependencies.

或者,如果您不使用 Boot 的自动配置,您可以按以下方式手动注册它:

Or, if you are not using Boot’s auto-configuration, you can register it manually in the following way:

  • 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 来添加 authenticationauthorization

To customize the interceptor itself, use RSocketSecurity to add rsocket-authentication and rsocket-authorization.

RSocket Authentication

RSocket 认证是使用 AuthenticationPayloadInterceptor 完成的,它作为调用 ReactiveAuthenticationManager 实例的控制器。

RSocket authentication is performed with AuthenticationPayloadInterceptor, which acts as a controller to invoke a ReactiveAuthenticationManager instance.

Authentication at Setup versus Request Time

一般来说,认证可以在建立时间或请求时间或两者发生。

Generally, authentication can occur at setup time or at request time or both.

在建立时间进行认证在少数场景中是有意义的。常见的场景是一个用户(例如移动连接)使用 RSocket 连接。在这种情况下,只有单个用户使用连接,这样一来就可以在连接时间认证一次。

Authentication at setup time makes sense in a few scenarios. A common scenarios is when a single user (such as a mobile connection) uses an RSocket connection. In this case, only a single user uses the connection, so authentication can be done once at connection time.

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

In a scenario where the RSocket connection is shared, it makes sense to send credentials on each request. For example, a web application that connects to an RSocket server as a downstream service would make a single connection that all users use. In this case, if the RSocket server needs to perform authorization based on the web application’s users credentials, authentication for each request makes sense.

在某些场景中,在建立时间和每个请求时进行认证是有意义的。考虑之前描述的 Web 应用程序。如果我们需要将连接限制到 Web 应用程序本身,我们可以提供具有 SETUP 授权的凭证。然后每个用户可能具有不同的权限,但没有 SETUP 权限。这意味着单个用户可以提出请求,但无法建立其他连接。

In some scenarios, authentication at both setup and for each request makes sense. Consider a web application, as described previously. If we need to restrict the connection to the web application itself, we can provide a credential with a SETUP authority at connection time. Then each user can have different authorities but not the SETUP authority. This means that individual users can make requests but not make additional connections.

Simple Authentication

Spring Security支持 Simple Authentication Metadata Extension

Spring Security has support for the Simple Authentication Metadata Extension.

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

Basic Authentication evolved into Simple Authentication and is only supported for backward compatibility. See RSocketSecurity.basicAuthentication(Customizer) for setting it up.

RSocket 接收器可以使用 AuthenticationPayloadExchangeConverter 解码凭证,它通过使用 DSL 的 simpleAuthentication 部分自动设置。以下示例显示了一个明确配置:

The RSocket receiver can decode the credentials by using AuthenticationPayloadExchangeConverter, which is automatically setup by using the simpleAuthentication portion of the DSL. The following example shows an explicit configuration:

  • 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

The RSocket sender can send credentials by using SimpleAuthenticationEncoder, which you can add to Spring’s RSocketStrategies.

  • Java

  • Kotlin

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

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

You can then use it to send a username and password to the receiver in the setup:

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

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

Alternatively or additionally, a username and password can be sent in a request.

  • 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做出授权决策。

Spring Security has support for the Bearer Token Authentication Metadata Extension. The support comes in the form of authenticating a JWT (determining that the JWT is valid) and then using the JWT to make authorization decisions.

RSocket 接收器可以使用 BearerPayloadExchangeConverter(由 DSL 的 jwt 部分自动设置)解码凭证。以下清单显示了一个示例配置:

The RSocket receiver can decode the credentials by using BearerPayloadExchangeConverter, which is automatically setup by using the jwt portion of the DSL. The following listing shows an example configuration:

  • 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。以下是可以找到从颁发者创建它的示例:

The configuration above relies on the existence of a ReactiveJwtDecoder @Bean being present. An example of creating one from the issuer can be found below:

  • 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。以下示例在 setup 时间发送令牌:

The RSocket sender does not need to do anything special to send the token, because the value is a simple String. The following example sends the token at setup time:

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

或者此外,你可以在请求中发送令牌:

Alternatively or additionally, you can send the token in a request:

  • 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 Authorization

RSocket 授权通过 AuthorizationPayloadInterceptor 执行,它充当控制器以调用 ReactiveAuthorizationManager 实例。你可以使用 DSL 根据 PayloadExchange 设置授权规则。以下清单显示了一个示例配置:

RSocket authorization is performed with AuthorizationPayloadInterceptor, which acts as a controller to invoke a ReactiveAuthorizationManager instance. You can use the DSL to set up authorization rules based upon the PayloadExchange. The following listing shows an example configuration:

  • 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 Setting up a connection requires the ROLE_SETUP authority.
2 If the route is fetch.profile.me, authorization only requires the user to be authenticated.
3 In this rule, we set up a custom matcher, where authorization requires the user to have the ROLE_CUSTOM authority.
4 This rule uses custom authorization. The matcher expresses a variable with a name of username that is made available in the context. A custom authorization rule is exposed in the checkFriends method.
5 This rule ensures that a request that does not already have a rule requires the user to be authenticated. A request is where the metadata is included. It would not include additional payloads.
6 This rule ensures that any exchange that does not already have a rule is allowed for anyone. In this example, it means that payloads that have no metadata also have no authorization rules.

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

Note that authorization rules are performed in order. Only the first authorization rule that matches is invoked.