RSocket 安全
Spring Security 的 RSocket 支持依赖于一个 SocketAcceptorInterceptor。安全性的主要入口点在 PayloadSocketAcceptorInterceptor 中,它将 RSocket API 适配为允许使用 PayloadInterceptor 实现拦截 PayloadExchange。
以下示例显示了一个最小的RSocket Security配置:
-
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-授权以要求任何请求都需要经过认证的用户。
添加 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。
基本认证已演变为简单认证,目前仅出于向后兼容的目的而支持。有关设置方法,请参见 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 Authentication Metadata Extension。这种支持的形式是验证 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。下面是一个从 issuer 创建它的示例:
- 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 发送方不需要做任何特殊操作来发送 token,因为该值是一个简单的 String。以下示例在设置时发送 token:
- 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 -> 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
设置连接需要
ROLE_SETUP权限。如果路由是
fetch.profile.me,授权只需要用户通过身份验证。在此规则中,我们设置了一个自定义匹配器,其中授权要求用户具有
ROLE_CUSTOM权限。该规则使用自定义授权。匹配器表达了一个名为
username的变量,该变量在context中可用。自定义授权规则在checkFriends方法中公开。该规则确保没有现有规则的请求需要用户进行身份验证。请求中包含元数据,但不包括额外的有效载荷。
该规则确保任何没有现有规则的交换对任何人都允许。在此示例中,这意味着没有元数据的有效载荷也没有授权规则。
请注意,授权规则是按顺序执行的。只有第一个匹配的授权规则会被调用。