Skip to content

Commit

Permalink
Add initial reactive redis session support
Browse files Browse the repository at this point in the history
  • Loading branch information
skivol committed Aug 3, 2020
1 parent 7d8d8ca commit 48c2bac
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 27 deletions.
1 change: 1 addition & 0 deletions autoconfigure-adapter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
compileOnly("javax.servlet:javax.servlet-api")
compileOnly("org.springframework:spring-webflux")
compileOnly("org.springframework.boot:spring-boot-starter-security")
compileOnly("org.springframework.session:spring-session-data-redis")
compileOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo")
compileOnly("org.springframework.data:spring-data-mongodb")
compileOnly("org.mongodb:mongodb-driver-reactivestreams")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.springframework.boot.autoconfigure.session;

import org.springframework.boot.autoconfigure.session.RedisReactiveSessionConfiguration.SpringBootRedisWebSessionConfiguration;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;

/**
* {@link ApplicationContextInitializer} adapter for {@link RedisReactiveSessionConfiguration}.
*/
public class RedisReactiveSessionConfigurationInitializer {

private final SessionProperties sessionProperties;
private final RedisSessionProperties redisSessionProperties;

public RedisReactiveSessionConfigurationInitializer(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) {
this.sessionProperties = sessionProperties;
this.redisSessionProperties = redisSessionProperties;
}

public void initialize(GenericApplicationContext context) {
SpringBootRedisWebSessionConfiguration springBootRedisWebSessionConfiguration = new SpringBootRedisWebSessionConfiguration();

springBootRedisWebSessionConfiguration.customize(sessionProperties, redisSessionProperties);

context.registerBean(ReactiveSessionRepository.class, () -> {
springBootRedisWebSessionConfiguration.setRedisConnectionFactory(
context.getBeanProvider(ReactiveRedisConnectionFactory.class), // TODO use @SpringSessionRedisConnectionFactory annotation qualifier
context.getBeanProvider(ReactiveRedisConnectionFactory.class)
);
springBootRedisWebSessionConfiguration.setDefaultRedisSerializer(
(RedisSerializer<Object>) context.getBeanProvider(ResolvableType.forClassWithGenerics(RedisSerializer.class, Object.class)).getIfAvailable()
);
springBootRedisWebSessionConfiguration.setSessionRepositoryCustomizer(
context.getBeanProvider(ResolvableType.forClassWithGenerics(ReactiveSessionRepositoryCustomizer.class, ReactiveRedisSessionRepository.class))
);
return springBootRedisWebSessionConfiguration.sessionRepository();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.springframework.boot.autoconfigure.session;

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionIdResolver;
import org.springframework.web.server.session.WebSessionManager;

/**
* {@link ApplicationContextInitializer} adapter for {@link SessionAutoConfiguration} and
* {@link EnableSpringWebSession}.
*/
public class SessionInitializer {
public void initialize(GenericApplicationContext context) {
// SpringWebSessionConfiguration
context.registerBean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, WebSessionManager.class, () -> {
SpringWebSessionConfiguration springWebSessionConfiguration = new SpringWebSessionConfiguration();
WebSessionManager webSessionManager = springWebSessionConfiguration.webSessionManager(
(ReactiveSessionRepository<? extends Session>) context.getBeanProvider(
ResolvableType.forClassWithGenerics(ReactiveSessionRepository.class, Session.class)
).getIfAvailable()
);
WebSessionIdResolver webSessionIdResolver = context.getBeanProvider(WebSessionIdResolver.class).getIfAvailable();
if (webSessionIdResolver != null) {
((DefaultWebSessionManager)webSessionManager).setSessionIdResolver(webSessionIdResolver);
}
return webSessionManager;
});
}
}
2 changes: 2 additions & 0 deletions kofu/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
compileOnly("org.springframework:spring-webflux")
compileOnly("org.springframework.boot:spring-boot-starter-security")
compileOnly("org.springframework.security.dsl:spring-security-kotlin-dsl:0.0.1.BUILD-SNAPSHOT") // needed until spring-security 5.4
compileOnly("org.springframework.session:spring-session-data-redis")
compileOnly("de.flapdoodle.embed:de.flapdoodle.embed.mongo")
compileOnly("org.springframework.data:spring-data-mongodb")
compileOnly("org.springframework.data:spring-data-r2dbc")
Expand All @@ -39,6 +40,7 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security.dsl:spring-security-kotlin-dsl:0.0.1.BUILD-SNAPSHOT") // needed until spring-security 5.4
testImplementation("org.springframework.session:spring-session-data-redis")
testImplementation("org.springframework.boot:spring-boot-starter-tomcat")
testImplementation("org.springframework.boot:spring-boot-starter-undertow")
testImplementation("org.springframework.boot:spring-boot-starter-jetty")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.springframework.fu.kofu.session

import org.springframework.boot.autoconfigure.session.RedisReactiveSessionConfigurationInitializer
import org.springframework.boot.autoconfigure.session.RedisSessionProperties
import org.springframework.boot.autoconfigure.session.SessionInitializer
import org.springframework.boot.autoconfigure.session.SessionProperties
import org.springframework.context.support.GenericApplicationContext
import org.springframework.fu.kofu.AbstractDsl
import org.springframework.fu.kofu.ConfigurationDsl
import org.springframework.session.SaveMode
import java.time.Duration

/**
* Kofu DSL for spring-session.
*
* Configure spring-session.
*
* Required dependencies can be retrieve using `org.springframework.boot:spring-boot-starter-session`.
*
* @author Ivan Skachkov
*/
class SessionDsl(private val init: SessionDsl.() -> Unit) : ConfigurationDsl({}) {

override fun initialize(context: GenericApplicationContext) {
super.initialize(context)
init()
SessionInitializer().initialize(context)
}
}

/**
* Configure spring-session.
*
* Requires `org.springframework.boot:spring-boot-starter-session` dependency.
*
* @sample org.springframework.fu.kofu.samples.sessionDsl
* @author Ivan Skachkov
*/
fun ConfigurationDsl.session(dsl: SessionDsl.() -> Unit = {}) {
SessionDsl(dsl).initialize(context)
}

/**
* Kofu DSL for spring-session-data-redis.
*
* Configure spring-session-data-redis.
*
* Required dependencies can be retrieve using `org.springframework.session:spring-session-data-redis`.
*
* @see SessionProperties, RedisSessionProperties, org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession
* @author Ivan Skachkov
*/
class ReactiveRedisDsl(
private val init: ReactiveRedisDsl.() -> Unit,
private val sessionProperties: SessionProperties,
private val redisSessionProperties: RedisSessionProperties
) : AbstractDsl() {

var maxInactiveIntervalInSeconds: Duration
get() = sessionProperties.timeout
set(value) {
sessionProperties.timeout = value
}

/**
* Namespace for keys used to store sessions.
*/
var redisNamespace: String
get() = redisSessionProperties.namespace
set(value) {
redisSessionProperties.namespace = value
}

/**
* Sessions save mode. Determines how session changes are tracked and saved to the
* session store.
*/
var saveMode: SaveMode
get() = redisSessionProperties.saveMode
set(value) {
redisSessionProperties.saveMode = value
}

override fun initialize(context: GenericApplicationContext) {
super.initialize(context)
init()
RedisReactiveSessionConfigurationInitializer(sessionProperties, redisSessionProperties).initialize(context)
}
}

/**
* @see ReactiveRedisDsl
*/
fun SessionDsl.reactiveRedis(dsl: ReactiveRedisDsl.() -> Unit = {}) {
val sessionProperties = configurationProperties(prefix = "spring.session", defaultProperties = SessionProperties())
val redisSessionProperties = configurationProperties(prefix = "spring.session.redis", defaultProperties = RedisSessionProperties())
ReactiveRedisDsl(dsl, sessionProperties, redisSessionProperties).initialize(context)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.springframework.fu.kofu.ConfigurationDsl
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.config.annotation.web.reactive.SecurityInitializer
import org.springframework.security.config.annotation.web.reactive.WebFluxSecurityInitializer
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import org.springframework.security.crypto.password.PasswordEncoder
Expand Down Expand Up @@ -50,6 +51,11 @@ class SecurityDsl(private val init: SecurityDsl.() -> Unit) : AbstractDsl() {

var http: ServerHttpSecurityDsl.() -> Unit = {}

/**
* For customizations not available through spring-security-dsl
*/
var securityCustomizer: (http: ServerHttpSecurity) -> ServerHttpSecurity = { it }

override fun initialize(context: GenericApplicationContext) {
super.initialize(context)
init()
Expand All @@ -62,6 +68,8 @@ class SecurityDsl(private val init: SecurityDsl.() -> Unit) : AbstractDsl() {
)
securityInitializer.initialize(context)

securityCustomizer.invoke(securityInitializer.httpSecurity)

val chain = securityInitializer.httpSecurity.invoke(http)

val webFlux = context is ReactiveWebServerApplicationContext
Expand Down
42 changes: 42 additions & 0 deletions kofu/src/test/kotlin/org/springframework/fu/kofu/TestUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.fu.kofu

import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import java.nio.charset.Charset
import java.util.*

fun userDetailsService(username: String, password: String): MapReactiveUserDetailsService {
@Suppress("DEPRECATION")
val user = User.withDefaultPasswordEncoder()
.username(username)
.password(password)
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}

val username = "user"
val password = "password"
fun repoAuthenticationManager(): UserDetailsRepositoryReactiveAuthenticationManager {
return UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService(username, password))
}

fun basicAuth() =
Base64.getEncoder().encode("$username:$password".toByteArray())?.toString(Charset.defaultCharset())
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.fu.kofu.samples

import org.springframework.fu.kofu.reactiveWebApplication
import org.springframework.fu.kofu.session.reactiveRedis
import org.springframework.fu.kofu.webflux.security
import org.springframework.fu.kofu.session.session
import org.springframework.fu.kofu.webflux.webFlux
import org.springframework.session.SaveMode
import java.time.Duration

fun sessionDsl() {
reactiveWebApplication {
session {
reactiveRedis {
maxInactiveIntervalInSeconds = Duration.ofSeconds(Long.MAX_VALUE)
redisNamespace = "spring:session" // default
saveMode = SaveMode.ON_SET_ATTRIBUTE // default
}
}
security {
http = {
anonymous { }
authorizeExchange {
authorize("/view", hasRole("USER"))
authorize("/public-view", permitAll)
}
headers {}
logout {}
}
}
webFlux { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,25 @@
package org.springframework.fu.kofu.webflux

import org.junit.jupiter.api.Test
import org.springframework.fu.kofu.basicAuth
import org.springframework.fu.kofu.localServerPort
import org.springframework.fu.kofu.reactiveWebApplication
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.fu.kofu.repoAuthenticationManager
import org.springframework.test.web.reactive.server.WebTestClient
import java.nio.charset.Charset
import java.time.Duration
import java.util.*


/**
* @author Jonas Bark, Ivan Skachkov
* @author Jonas Bark
* @author Ivan Skachkov
*/
class SecurityDslTests {

@Test
fun `Check spring-security configuration DSL`() {

val username = "user"
val password = "password"
val repoAuthenticationManager =
UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService(username, password))

val app = reactiveWebApplication {
security {
authenticationManager = repoAuthenticationManager
authenticationManager = repoAuthenticationManager()

http = {
authorizeExchange {
Expand Down Expand Up @@ -72,23 +64,10 @@ class SecurityDslTests {
client.get().uri("/view").exchange()
.expectStatus().isUnauthorized

val basicAuth =
Base64.getEncoder().encode("$username:$password".toByteArray())?.toString(Charset.defaultCharset())
client.get().uri("/view").header("Authorization", "Basic $basicAuth").exchange()
client.get().uri("/view").header("Authorization", "Basic ${basicAuth()}").exchange()
.expectStatus().is2xxSuccessful

close()
}
}

private fun userDetailsService(username: String, password: String): MapReactiveUserDetailsService {
@Suppress("DEPRECATION")
val user = User.withDefaultPasswordEncoder()
.username(username)
.password(password)
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}

}
Loading

0 comments on commit 48c2bac

Please sign in to comment.