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

RSocket 安全性

Spring Security 的 RSocket 支持依赖于SocketAcceptorInterceptor. 安全的主要入口点是PayloadSocketAcceptorInterceptor,它调整 RSocket API 以允许拦截PayloadExchangePayloadInterceptor实现。spring-doc.cadn.net.cn

以下示例显示了最小的 RSocket Security 配置:spring-doc.cadn.net.cn

最小 RSocket 安全配置

您可以在下面找到最小的 RSocket Security 配置: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)
    }
}

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

添加 SecuritySocketAcceptorInterceptor

要使 Spring Security 正常工作,我们需要申请SecuritySocketAcceptorInterceptorServerRSocketFactory. 这样做将我们的PayloadSocketAcceptorInterceptor使用 RSocket 基础架构。 在 Spring Boot 应用程序中,您可以使用RSocketSecurityAutoConfiguration使用以下代码: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)
        }
    }
}

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 应用程序本身的连接,我们可以提供一个带有SETUPauthority 在连接时。 然后,每个用户可以拥有不同的权限,但不能拥有SETUP柄。 这意味着单个用户可以发出请求,但不能进行其他连接。spring-doc.cadn.net.cn

简单身份验证

基本身份验证演变为简单身份验证,仅支持向后兼容。 看RSocketSecurity.basicAuthentication(Customizer)进行设置。spring-doc.cadn.net.cn

RSocket 接收器可以使用AuthenticationPayloadExchangeConverter,该 API 是使用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()
}

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 支持不记名令牌身份验证元数据扩展。 支持的形式包括对 JWT 进行身份验证(确定 JWT 有效),然后使用 JWT 做出授权决策。spring-doc.cadn.net.cn

RSocket 接收器可以使用BearerPayloadExchangeConverter,该 API 是使用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在场。 下面提供了从颁发者创建一个 ID 的示例: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")
}

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)

或者,您也可以在请求中发送令牌: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 在此规则中,我们设置了一个自定义匹配程序,其中 authorization 要求用户具有ROLE_CUSTOM柄。
4 此规则使用自定义授权。 匹配器表示名称为username中提供了context. 自定义授权规则在checkFriends方法。
5 此规则可确保尚无规则的请求要求对用户进行身份验证。 请求是包含元数据的位置。 它不会包含额外的负载。
6 此规则确保任何人都允许任何尚未制定规则的交易所。 在此示例中,这意味着没有元数据的负载也没有授权规则。

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