mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-11-04 08:39:05 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			465 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			465 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
[[rsocket]]
 | 
						|
= RSocket Security
 | 
						|
 | 
						|
Spring Security's RSocket support relies on a `SocketAcceptorInterceptor`.
 | 
						|
The main entry point into security is in `PayloadSocketAcceptorInterceptor`, which adapts the RSocket APIs to allow intercepting a `PayloadExchange` with `PayloadInterceptor` implementations.
 | 
						|
 | 
						|
The following example shows a minimal RSocket Security configuration:
 | 
						|
 | 
						|
* Hello RSocket {gh-samples-url}/reactive/rsocket/hello-security[hellorsocket]
 | 
						|
* https://github.com/rwinch/spring-flights/tree/security[Spring Flights]
 | 
						|
 | 
						|
 | 
						|
== Minimal RSocket Security Configuration
 | 
						|
 | 
						|
You can find a minimal RSocket Security configuration below:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableRSocketSecurity
 | 
						|
public class HelloRSocketSecurityConfig {
 | 
						|
 | 
						|
	@Bean
 | 
						|
	public MapReactiveUserDetailsService userDetailsService() {
 | 
						|
		UserDetails user = User.withDefaultPasswordEncoder()
 | 
						|
			.username("user")
 | 
						|
			.password("user")
 | 
						|
			.roles("USER")
 | 
						|
			.build();
 | 
						|
		return new MapReactiveUserDetailsService(user);
 | 
						|
	}
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableRSocketSecurity
 | 
						|
open class HelloRSocketSecurityConfig {
 | 
						|
    @Bean
 | 
						|
    open fun userDetailsService(): MapReactiveUserDetailsService {
 | 
						|
        val user = User.withDefaultPasswordEncoder()
 | 
						|
            .username("user")
 | 
						|
            .password("user")
 | 
						|
            .roles("USER")
 | 
						|
            .build()
 | 
						|
        return MapReactiveUserDetailsService(user)
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
This configuration enables <<rsocket-authentication-simple,simple authentication>> and sets up <<rsocket-authorization,rsocket-authorization>> to require an authenticated user for any request.
 | 
						|
 | 
						|
== Adding SecuritySocketAcceptorInterceptor
 | 
						|
 | 
						|
For Spring Security to work, we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
 | 
						|
Doing so connects our `PayloadSocketAcceptorInterceptor` with the RSocket infrastructure.
 | 
						|
In a Spring Boot application, you can do this automatically by using `RSocketSecurityAutoConfiguration` with the following code:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Bean
 | 
						|
RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) {
 | 
						|
    return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor));
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Bean
 | 
						|
fun springSecurityRSocketSecurity(interceptor: SecuritySocketAcceptorInterceptor): RSocketServerCustomizer {
 | 
						|
    return RSocketServerCustomizer { server ->
 | 
						|
        server.interceptors { registry ->
 | 
						|
            registry.forSocketAcceptor(interceptor)
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
[[rsocket-authentication]]
 | 
						|
== RSocket Authentication
 | 
						|
 | 
						|
RSocket authentication is performed with `AuthenticationPayloadInterceptor`, which acts as a controller to invoke a `ReactiveAuthenticationManager` instance.
 | 
						|
 | 
						|
[[rsocket-authentication-setup-vs-request]]
 | 
						|
=== Authentication at Setup versus Request Time
 | 
						|
 | 
						|
Generally, authentication can occur at setup time or at request time or both.
 | 
						|
 | 
						|
Authentication at setup time makes sense in a few scenarios.
 | 
						|
A common scenarios is when a single user (such as a mobile connection) uses an RSocket connection.
 | 
						|
In this case, only a single user uses the connection, so authentication can be done once at connection time.
 | 
						|
 | 
						|
In a scenario where the RSocket connection is shared, it makes sense to send credentials on each request.
 | 
						|
For example, a web application that connects to an RSocket server as a downstream service would make a single connection that all users use.
 | 
						|
In this case, if the RSocket server needs to perform authorization based on the web application's users credentials, authentication for each request makes sense.
 | 
						|
 | 
						|
In some scenarios, authentication at both setup and for each request makes sense.
 | 
						|
Consider a web application, as described previously.
 | 
						|
If we need to restrict the connection to the web application itself, we can provide a credential with a `SETUP` authority at connection time.
 | 
						|
Then each user can have different authorities but not the `SETUP` authority.
 | 
						|
This means that individual users can make requests but not make additional connections.
 | 
						|
 | 
						|
[[rsocket-authentication-simple]]
 | 
						|
=== Simple Authentication
 | 
						|
 | 
						|
Spring Security has support for the https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md[Simple Authentication Metadata Extension].
 | 
						|
 | 
						|
[NOTE]
 | 
						|
====
 | 
						|
Basic Authentication evolved into Simple Authentication and is only supported for backward compatibility.
 | 
						|
See `RSocketSecurity.basicAuthentication(Customizer)` for setting it up.
 | 
						|
====
 | 
						|
 | 
						|
The RSocket receiver can decode the credentials by using `AuthenticationPayloadExchangeConverter`, which is automatically setup by using the `simpleAuthentication` portion of the DSL.
 | 
						|
The following example shows an explicit configuration:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Bean
 | 
						|
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
 | 
						|
	rsocket
 | 
						|
		.authorizePayload(authorize ->
 | 
						|
			authorize
 | 
						|
					.anyRequest().authenticated()
 | 
						|
					.anyExchange().permitAll()
 | 
						|
		)
 | 
						|
		.simpleAuthentication(Customizer.withDefaults());
 | 
						|
	return rsocket.build();
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Bean
 | 
						|
open fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
 | 
						|
    rsocket
 | 
						|
        .authorizePayload { authorize -> authorize
 | 
						|
                .anyRequest().authenticated()
 | 
						|
                .anyExchange().permitAll()
 | 
						|
        }
 | 
						|
        .simpleAuthentication(withDefaults())
 | 
						|
    return rsocket.build()
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
The RSocket sender can send credentials by using `SimpleAuthenticationEncoder`, which you can add to Spring's `RSocketStrategies`.
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
RSocketStrategies.Builder strategies = ...;
 | 
						|
strategies.encoder(new SimpleAuthenticationEncoder());
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
var strategies: RSocketStrategies.Builder = ...
 | 
						|
strategies.encoder(SimpleAuthenticationEncoder())
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
You can then use it to send a username and password to the receiver in the setup:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
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);
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
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)
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
Alternatively or additionally, a username and password can be sent in a request.
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
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)
 | 
						|
	);
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
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>()
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
[[rsocket-authentication-jwt]]
 | 
						|
=== JWT
 | 
						|
 | 
						|
Spring Security has support for the https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Bearer.md[Bearer Token Authentication Metadata Extension].
 | 
						|
The support comes in the form of authenticating a JWT (determining that the JWT is valid) and then using the JWT to make authorization decisions.
 | 
						|
 | 
						|
The RSocket receiver can decode the credentials by using `BearerPayloadExchangeConverter`, which is automatically setup by using the `jwt` portion of the DSL.
 | 
						|
The following listing shows an example configuration:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Bean
 | 
						|
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
 | 
						|
	rsocket
 | 
						|
		.authorizePayload(authorize ->
 | 
						|
			authorize
 | 
						|
				.anyRequest().authenticated()
 | 
						|
				.anyExchange().permitAll()
 | 
						|
		)
 | 
						|
		.jwt(Customizer.withDefaults());
 | 
						|
	return rsocket.build();
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@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:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Bean
 | 
						|
ReactiveJwtDecoder jwtDecoder() {
 | 
						|
	return ReactiveJwtDecoders
 | 
						|
		.fromIssuerLocation("https://example.com/auth/realms/demo");
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@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 a simple `String`.
 | 
						|
The following example sends the token at setup time:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
MimeType authenticationMimeType =
 | 
						|
	MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
 | 
						|
BearerTokenMetadata token = ...;
 | 
						|
Mono<RSocketRequester> requester = RSocketRequester.builder()
 | 
						|
	.setupMetadata(token, authenticationMimeType)
 | 
						|
	.connectTcp(host, port);
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
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, you can send the token in a request:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
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)
 | 
						|
	);
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
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
 | 
						|
 | 
						|
RSocket authorization is performed with `AuthorizationPayloadInterceptor`, which acts as a controller to invoke a `ReactiveAuthorizationManager` instance.
 | 
						|
You can use the DSL to set up authorization rules based upon the `PayloadExchange`.
 | 
						|
The following listing shows an example configuration:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
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>
 | 
						|
	);
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
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 `ROLE_SETUP` authority.
 | 
						|
<2> If the route is `fetch.profile.me`, authorization only requires the user to be authenticated.
 | 
						|
<3> In this rule, we set up a custom matcher, where authorization requires the user to have the `ROLE_CUSTOM` authority.
 | 
						|
<4> This rule uses custom authorization.
 | 
						|
The matcher expresses a variable with a name of `username` that is made available in the `context`.
 | 
						|
A custom authorization rule is exposed in the `checkFriends` method.
 | 
						|
<5> This rule ensures that a request that does not already have a rule requires 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 also have no authorization rules.
 | 
						|
 | 
						|
Note that authorization rules are performed in order.
 | 
						|
Only the first authorization rule that matches is invoked.
 |