RSocket 安全
Spring Security 的 RSocket 支持依赖于 SocketAcceptorInterceptor。安全机制的主要入口点是 PayloadSocketAcceptorInterceptor,它适配了 RSocket API,允许通过 PayloadInterceptor 实现来拦截 PayloadExchange。
以下示例展示了最小化的 RSocket 安全配置:
-
Hello RSocket hellorsocket
最小化 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)
}
}
此配置启用了简单认证,并设置了rsocket-authorization,要求所有请求都必须来自已认证的用户。
添加 SecuritySocketAcceptorInterceptor
为了让Spring Security生效,我们需要将SecuritySocketAcceptorInterceptor应用到ServerRSocketFactory上。这样做可以将我们的PayloadSocketAcceptorInterceptor与RSocket基础设施连接起来。
当你包含正确的依赖项时,Spring Boot 会在 RSocketSecurityAutoConfiguration 中自动注册它。
或者,如果您没有使用 Boot 的自动配置,也可以通过以下方式手动注册:
- 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 应用程序的用户凭据执行授权,那么对每个请求进行身份验证是合理的。
在某些场景中,在建立连接时和每次请求时都进行身份验证是有意义的。以前面提到的 Web 应用程序为例。如果我们需要将连接限制在 Web 应用程序本身,可以在连接时提供一个具有 SETUP 权限的凭据。这样,每个用户可以拥有不同的权限,但不能拥有 SETUP 权限。这意味着单个用户可以发出请求,但不能建立额外的连接。
简单认证
Spring Security 支持 Simple Authentication Metadata Extension。
Basic Authentication 已演变为 Simple Authentication,目前仅出于向后兼容性考虑而提供支持。如需配置,请参阅 RSocketSecurity.basicAuthentication(Customizer)。
RSocket接收器可以通过使用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接收器可以通过使用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 的存在。以下示例展示了如何基于颁发者创建该 Bean:
- 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执行,它作为控制器来调用ReactiveAuthorizationManager实例。您可以使用DSL基于PayloadExchange设置授权规则。以下清单展示了一个配置示例:
- Java
- Kotlin
rsocket
.authorizePayload(authz ->
authz
.setup().hasRole("SETUP") 1
.route("fetch.profile.me").authenticated() 2
.matcher((payloadExchange) -> payloadExchange(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
建立连接需要
ROLE_SETUP权限。如果路由是
fetch.profile.me,授权仅要求用户已通过身份验证。在此规则中,我们设置了一个自定义匹配器,授权要求用户拥有
ROLE_CUSTOM权限。此规则使用自定义授权。匹配器表达了一个名为
username的变量,该变量在context中可用。自定义授权规则在checkFriends方法中公开。此规则确保尚未拥有规则的请求要求用户已通过身份验证。请求是指包含元数据的情况。它不会包含额外的有效载荷。
此规则确保任何尚未拥有规则的交换对任何人都被允许。在此示例中,这意味着没有元数据的有效载荷也没有授权规则。
请注意,授权规则是按顺序执行的。只有第一个匹配的授权规则会被调用。