mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-30 22:28:46 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			470 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			470 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.
 | |
| 
 | |
| Spring Boot registers it automatically in `RSocketSecurityAutoConfiguration` when you include {gh-samples-url}/reactive/rsocket/hello-security/build.gradle[the correct dependencies].
 | |
| 
 | |
| Or, if you are not using Boot's auto-configuration, you can register it manually in the following way:
 | |
| 
 | |
| [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)
 | |
|         }
 | |
|     }
 | |
| }
 | |
| ----
 | |
| ======
 | |
| 
 | |
| To customize the interceptor itself, use `RSocketSecurity` to add <<rsocket-authentication,authentication>> and <<rsocket-authorization,authorization>>.
 | |
| 
 | |
| [[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.
 |