对于最新的稳定版本,请使用 Spring Security 6.4.3! |
RSocket 安全性
Spring Security 的 RSocket 支持依赖于SocketAcceptorInterceptor
.
安全性的主要入口点位于PayloadSocketAcceptorInterceptor
它调整了 RSocket API 以允许拦截PayloadExchange
跟PayloadInterceptor
实现。
您可以找到一些演示以下代码的示例应用程序:
-
你好 RSocket hellorsocket
最小 RSocket 安全配置
您可以在下面找到最小的 RSocket Security 配置:
-
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
替换为以下代码。
-
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
authority 在连接时。
然后,每个用户将具有不同的权限,但不会SETUP
柄。
这意味着单个用户可以发出请求,但不能进行其他连接。
简单身份验证
Spring Security 支持简单身份验证元数据扩展。
Basic Authentication 草稿演变为 Simple Authentication,并且仅支持向后兼容。
看 |
RSocket 接收器可以使用AuthenticationPayloadExchangeConverter
,该 API 使用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 has support for Bearer Token Authentication Metadata Extension.
The support comes in the form of authenticating a JWT (determining the JWT is valid) and then using the JWT to make authorization decisions.
The RSocket receiver can decode the credentials using BearerPayloadExchangeConverter
which is automatically setup using the jwt
portion of the DSL.
An example configuration can be found below:
-
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()
}
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")
}
The RSocket sender does not need to do anything special to send the token because the value is just a simple String.
For example, the token can be sent 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, the token can be sent 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 authorization is performed with AuthorizationPayloadInterceptor
which acts as a controller to invoke a ReactiveAuthorizationManager
instance.
The DSL can be used to setup authorization rules based upon the PayloadExchange
.
An example configuration can be found below:
-
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 authority ROLE_SETUP
2
If the route is fetch.profile.me
authorization only requires the user be authenticated
3
In this rule we setup a custom matcher where authorization requires the user to have the authority ROLE_CUSTOM
4
This rule leverages custom authorization.
The matcher expresses a variable with the name username
that is made available in the context
.
A custom authorization rule is exposed in the checkFriends
method.
5
This rule ensures that request that does not already have a rule will require 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 have no authorization rules.
It is important to understand that authorization rules are performed in order.
Only the first authorization rule that matches will be invoked.