Add remaining Kotlin samples to reference docs

Closes gh-8172
This commit is contained in:
Eleftheria Stein 2021-06-24 11:49:13 +02:00
parent 94a3adb928
commit da9d7414bd
8 changed files with 1029 additions and 63 deletions

View File

@ -10,7 +10,9 @@ The easiest way to ensure that CORS is handled first is to use the `CorsWebFilte
Users can integrate the `CorsWebFilter` with Spring Security by providing a `CorsConfigurationSource`.
For example, the following will integrate CORS support within Spring Security:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
CorsConfigurationSource corsConfigurationSource() {
@ -23,9 +25,26 @@ CorsConfigurationSource corsConfigurationSource() {
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("https://example.com")
configuration.allowedMethods = listOf("GET", "POST")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
----
====
The following will disable the CORS integration within Spring Security:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
@ -35,3 +54,18 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
cors {
disable()
}
}
}
----
====

View File

@ -14,7 +14,9 @@ You can find a few sample applications that demonstrate the code below:
You can find a minimal RSocket Security configuration below:
[source,java]
====
.Java
[source,java,role="primary"]
-----
@Configuration
@EnableRSocketSecurity
@ -32,6 +34,25 @@ public class HelloRSocketSecurityConfig {
}
-----
.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
@ -86,7 +107,9 @@ 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.
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
@ -101,17 +124,45 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
}
----
.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`.
[source,java]
====
.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:
[source,java]
====
.Java
[source,java,role="primary"]
----
MimeType authenticationMimeType =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
@ -122,9 +173,24 @@ Mono<RSocketRequester> requester = RSocketRequester.builder()
.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.
[source,java]
====
.Java
[source,java,role="primary"]
----
Mono<RSocketRequester> requester;
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
@ -138,6 +204,26 @@ public Mono<AirportLocation> findRadar(String code) {
}
----
.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
@ -147,7 +233,9 @@ The support comes in the form of authenticating a JWT (determining the JWT is va
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:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
@ -162,10 +250,28 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
}
----
.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:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
ReactiveJwtDecoder jwtDecoder() {
@ -174,10 +280,23 @@ ReactiveJwtDecoder jwtDecoder() {
}
----
.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:
[source,java]
====
.Java
[source,java,role="primary"]
----
MimeType authenticationMimeType =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
@ -187,9 +306,24 @@ Mono<RSocketRequester> requester = RSocketRequester.builder()
.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.
[source,java]
====
.Java
[source,java,role="primary"]
----
MimeType authenticationMimeType =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
@ -205,6 +339,24 @@ public Mono<AirportLocation> findRadar(String code) {
}
----
.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
@ -212,7 +364,9 @@ RSocket authorization is performed with `AuthorizationPayloadInterceptor` which
The DSL can be used to setup authorization rules based upon the `PayloadExchange`.
An example configuration can be found below:
[[source,java]]
====
.Java
[source,java,role="primary"]
----
rsocket
.authorizePayload(authz ->
@ -227,6 +381,23 @@ rsocket
.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`

File diff suppressed because it is too large Load Diff

View File

@ -134,7 +134,9 @@ You can configure multiple `SecurityWebFilterChain` instances to separate config
For example, you can isolate configuration for URLs that start with `/api`, like so:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Configuration
@EnableWebFluxSecurity
@ -171,6 +173,46 @@ static class MultiSecurityHttpConfig {
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebFluxSecurity
open class MultiSecurityHttpConfig {
@Order(Ordered.HIGHEST_PRECEDENCE) <1>
@Bean
open fun apiHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) <2>
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { } <3>
}
}
}
@Bean
open fun webHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { <4>
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
httpBasic { } <5>
}
}
@Bean
open fun userDetailsService(): ReactiveUserDetailsService {
return MapReactiveUserDetailsService(
PasswordEncodedUser.user(), PasswordEncodedUser.admin()
)
}
}
----
====
<1> Configure a `SecurityWebFilterChain` with an `@Order` to specify which `SecurityWebFilterChain` Spring Security should consider first
<2> Use `PathPatternParserServerWebExchangeMatcher` to state that this `SecurityWebFilterChain` will only apply to URL paths that start with `/api/`
<3> Specify the authentication mechanisms that will be used for `/api/**` endpoints

View File

@ -4,7 +4,9 @@
Similar to <<servlet-x509,Servlet X.509 authentication>>, reactive x509 authentication filter allows extracting an authentication token from a certificate provided by a client.
Below is an example of a reactive x509 security configuration:
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
@ -17,11 +19,28 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
x509 { }
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
----
====
In the configuration above, when neither `principalExtractor` nor `authenticationManager` is provided defaults will be used. The default principal extractor is `SubjectDnX509PrincipalExtractor` which extracts the CN (common name) field from a certificate provided by a client. The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager` which performs user account validation, checking that user account with a name extracted by `principalExtractor` exists and it is not locked, disabled, or expired.
The next example demonstrates how these defaults can be overridden.
[source,java]
====
.Java
[source,java,role="primary"]
----
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
@ -47,6 +66,30 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
val customPrincipalExtractor = SubjectDnX509PrincipalExtractor()
customPrincipalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)")
val customAuthenticationManager = ReactiveAuthenticationManager { authentication: Authentication ->
authentication.isAuthenticated = "Trusted Org Unit" == authentication.name
Mono.just(authentication)
}
return http {
x509 {
principalExtractor = customPrincipalExtractor
authenticationManager = customAuthenticationManager
}
authorizeExchange {
authorize(anyExchange, authenticated)
}
}
}
----
====
In this example, a username is extracted from the OU field of a client certificate instead of CN, and account lookup using `ReactiveUserDetailsService` is not performed at all. Instead, if the provided certificate issued to an OU named "Trusted Org Unit", a request will be authenticated.
For an example of configuring Netty and `WebClient` or `curl` command-line tool to use mutual TLS and enable X.509 authentication, please refer to https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509.

View File

@ -108,7 +108,9 @@ https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc
This means that a construct like this one:
[source,java]
====
.Java
[source,java,role="primary"]
----
@GetMapping("/")
public String method(Authentication authentication) {
@ -120,6 +122,20 @@ public String method(Authentication authentication) {
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@GetMapping("/")
fun method(authentication: Authentication?): String {
return if (authentication is AnonymousAuthenticationToken) {
"anonymous"
} else {
"not anonymous"
}
}
----
====
will always return "not anonymous", even for anonymous requests.
The reason is that Spring MVC resolves the parameter using `HttpServletRequest#getPrincipal`, which is `null` when the request is anonymous.

View File

@ -820,12 +820,22 @@ class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() {
Or similarly with method security:
[source,java]
====
.Java
[source,java,role="primary"]
----
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message> { }
----
====
[[oauth2resourceserver-jwt-authorization-extraction]]
==== Extracting Authorities Manually

View File

@ -4,7 +4,9 @@
This section demonstrates how to use Spring Security's Test support to test method based security.
We first introduce a `MessageService` that requires the user to be authenticated in order to access it.
[source,java]
====
.Java
[source,java,role="primary"]
----
public class HelloMessageService implements MessageService {
@ -17,6 +19,19 @@ public class HelloMessageService implements MessageService {
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
class HelloMessageService : MessageService {
@PreAuthorize("authenticated")
fun getMessage(): String {
val authentication: Authentication = SecurityContextHolder.getContext().authentication
return "Hello $authentication"
}
}
----
====
The result of `getMessage` is a String saying "Hello" to the current Spring Security `Authentication`.
An example of the output is displayed below.