对于最新的稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

RSocket 安全

Spring Security 的 RSocket 支持依赖于一个 SocketAcceptorInterceptor。 安全的主要入口点位于 PayloadSocketAcceptorInterceptor,它将 RSocket API 调整为允许使用 PayloadExchange 实现拦截 PayloadInterceptorspring-doc.cadn.net.cn

以下示例展示了最小的RSocket 安全配置:spring-doc.cadn.net.cn

最小化 RSocket 安全配置

您可以在下方找到一个最小化的RSocket安全配置:spring-doc.cadn.net.cn

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

此配置启用简单身份验证,并设置以要求任何请求都需要经过身份验证的用户。spring-doc.cadn.net.cn

添加 SecuritySocketAcceptorInterceptor

要使Spring Security生效,我们需要将SecuritySocketAcceptorInterceptor应用到ServerRSocketFactory。 这样做会将我们的PayloadSocketAcceptorInterceptor与RSocket基础设施连接起来。spring-doc.cadn.net.cn

Spring Boot 会在您包含正确依赖项时,自动在 https://github.com/spring-projects/spring-security-samples/tree/6.5.x/reactive/rsocket/hello-security/build.gradle 中注册它。spring-doc.cadn.net.cn

如果您没有使用 Boot 的自动配置,您可以以如下方式手动注册它:spring-doc.cadn.net.cn

@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添加身份验证授权spring-doc.cadn.net.cn

RSocket 身份验证

RSocket 认证是通过 AuthenticationPayloadInterceptor 完成的,它作为控制器来调用一个 ReactiveAuthenticationManager 实例。spring-doc.cadn.net.cn

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

一般而言,身份验证可以在初始化时进行或在请求时进行,或者两者都进行。spring-doc.cadn.net.cn

在设置时进行身份验证在几种场景下是合理的。 常见的场景是在使用 RSocket 连接时,只有一个用户(例如移动连接)使用该连接。 在这种情况下,只有单个用户使用连接,因此可以在连接时一次性完成身份验证。spring-doc.cadn.net.cn

在共享RSocket连接的情况下,向每个请求发送凭据是有意义的。 例如,一个连接到RSocket服务器作为下游服务的Web应用程序会建立一个所有用户都使用的单一连接。 在这种情况下,如果RSocket服务器需要基于Web应用程序的用户凭据进行授权,则对每个请求进行身份验证是有道理的。spring-doc.cadn.net.cn

在某些场景下,设置时和每次请求都需要进行身份验证是有意义的。 考虑一个如前所述的Web应用程序。 如果我们需要限制对该Web应用本身的访问,可以在连接时提供一个带有SETUP权限的凭据。 然后每个用户可以有不同的权限,但不包括SETUP权限。 这意味着个别用户可以发送请求但不能建立额外的连接。spring-doc.cadn.net.cn

简单认证

基本认证演进为简单认证,仅支持兼容性回退。 请参见RSocketSecurity.basicAuthentication(Customizer)以进行设置。spring-doc.cadn.net.cn

Spring框架中的RSocket接收器可以使用AuthenticationPayloadExchangeConverter来解码凭据,这通过在DSL的simpleAuthentication部分自动设置完成。 以下示例展示了显式配置:spring-doc.cadn.net.cn

@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()
}

The RSocket 发送器可以通过使用SimpleAuthenticationEncoder来发送凭证,您可以将其添加到Spring的RSocketStrategies中。spring-doc.cadn.net.cn

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

然后您可以使用它在设置中向接收方发送用户名和密码:spring-doc.cadn.net.cn

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)

可以发送用户名和密码作为请求的一部分,或者额外发送。spring-doc.cadn.net.cn

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-doc.cadn.net.cn

RSocket接收器可以通过使用BearerPayloadExchangeConverter来解码凭据,该转换器是通过DSL的jwt部分自动设置的。 下面的示例显示了配置的一个例子:spring-doc.cadn.net.cn

@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。 可以从发行者创建一个示例如下所示:spring-doc.cadn.net.cn

@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")
}

The RSocket 发送器无需为发送标记牌做任何特殊处理,因为该值是一个简单的 `String`。 以下示例在设置时发送标记牌:spring-doc.cadn.net.cn

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)

可以将Tokens在请求中发送:spring-doc.cadn.net.cn

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 设置授权规则。下面的示例配置展示了如何进行设置:spring-doc.cadn.net.cn

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 此规则确保任何没有现有规则的交换允许任何人。 在这个例子中,这意味着没有元数据的消息也没有授权规则。

注意授权规则是按顺序执行的。 只有第一个匹配的授权规则会被调用。spring-doc.cadn.net.cn