mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-30 22:28:46 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			415 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			415 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| [[rsocket]]
 | |
| = RSocket Security
 | |
| 
 | |
| Spring Security's RSocket support relies on a `SocketAcceptorInterceptor`.
 | |
| The main entry point into security is found in the `PayloadSocketAcceptorInterceptor` which adapts the RSocket APIs to allow intercepting a `PayloadExchange` with `PayloadInterceptor` implementations.
 | |
| 
 | |
| You can find a few sample applications that demonstrate the code below:
 | |
| 
 | |
| * 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:
 | |
| 
 | |
| ====
 | |
| .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`.
 | |
| This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
 | |
| In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
 | |
| 
 | |
| [source,java]
 | |
| ----
 | |
| @Bean
 | |
| RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) {
 | |
|     return (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 vs Request Time
 | |
| 
 | |
| Generally, authentication can occur at setup time and/or request time.
 | |
| 
 | |
| Authentication at setup time makes sense in a few scenarios.
 | |
| A common scenarios is when a single user (i.e. mobile connection) is leveraging an RSocket connection.
 | |
| In this case only a single user is leveraging 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 leverage.
 | |
| In this case, if the RSocket server needs to perform authorization based on the web application's users credentials per request makes sense.
 | |
| 
 | |
| In some scenarios authentication at setup and per 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 would 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 https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md[Simple Authentication Metadata Extension].
 | |
| 
 | |
| [NOTE]
 | |
| ====
 | |
| Basic Authentication drafts 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 using `AuthenticationPayloadExchangeConverter` which is automatically setup using the `simpleAuthentication` portion of the DSL.
 | |
| An explicit configuration can be found below.
 | |
| 
 | |
| ====
 | |
| .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 using `SimpleAuthenticationEncoder` which can be added to Spring's `RSocketStrategies`.
 | |
| 
 | |
| ====
 | |
| .Java
 | |
| [source,java,role="primary"]
 | |
| ----
 | |
| RSocketStrategies.Builder strategies = ...;
 | |
| strategies.encoder(new SimpleAuthenticationEncoder());
 | |
| ----
 | |
| 
 | |
| .Kotlin
 | |
| [source,kotlin,role="secondary"]
 | |
| ----
 | |
| var strategies: RSocketStrategies.Builder = ...
 | |
| strategies.encoder(SimpleAuthenticationEncoder())
 | |
| ----
 | |
| ====
 | |
| 
 | |
| It can then be used to send a username and password to the receiver in the setup:
 | |
| 
 | |
| ====
 | |
| .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.
 | |
| 
 | |
| ====
 | |
| .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 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 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
 | |
| [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:
 | |
| 
 | |
| ====
 | |
| .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 just a simple String.
 | |
| For example, the token can be sent at setup time:
 | |
| 
 | |
| ====
 | |
| .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, the token can be sent in a request.
 | |
| 
 | |
| ====
 | |
| .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.
 | |
| The DSL can be used to setup authorization rules based upon the `PayloadExchange`.
 | |
| An example configuration can be found below:
 | |
| 
 | |
| ====
 | |
| .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 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.
 |