Closed dreamstar-enterprises closed 3 weeks ago
Hi @dreamstar-enterprises, I don't quite understand the following sentence:
But the function below, in order to autowire WebSessionStore, needs to see a manual implementation of WebsessionStore, so what do I do?
Spring Session creates a WebSessionManager
bean, so you should be able to inject it. Would you be able to clarify? Or perhaps create a minimal, reproducible sample?
Hi Marcus, thanks for replying. I've parked my BFF server, as I had trouble with Redis serialising something. I'm stuck though on my Spring Auth Server, currently, and why it can't persist sessions to Redis. Please see: https://stackoverflow.com/questions/78829545/spring-auth-server-cannot-persist-session-csrf-cookies-to-azure-redis?noredirect=1#comment138986071_78829545
Hi Macus, sorry for the delay in getting back to you. I currently try to do this:
Websession Store
@Configuration
internal class WebSessionStoreConfig {
/**
* Adapts ReactiveRedisIndexedSessionRepository (which stores sessions in Redis) to be usable
* as a WebSessionStore in WebFlux.
*/
@Bean(name = ["webSessionStore"])
fun webSessionStore(
reactiveRedisIndexedSessionRepository: ReactiveRedisIndexedSessionRepository
): SpringSessionWebSessionStore<RedisSession> {
return SpringSessionWebSessionStore(reactiveRedisIndexedSessionRepository)
}
/**
* Configures how sessions are managed in WebFlux, using cookies to store session IDs
* and Redis to store session data
*/
@Bean(name = ["webSessionManager"])
fun webSessionManager(
cookieWebSessionIdResolver: CookieWebSessionIdResolver,
webSessionStore: SpringSessionWebSessionStore<RedisSession>
): WebSessionManager {
val sessionManager = DefaultWebSessionManager()
sessionManager.sessionStore = webSessionStore
sessionManager.sessionIdResolver = cookieWebSessionIdResolver
return sessionManager
}
}
Session Control
adapted from: https://docs.spring.io/spring-session/reference/configuration/redis.html#finding-all-user-sessions
@Component
internal class SessionControl(
private val reactiveSessionRegistry: CustomSpringSessionReactiveSessionRegistry<ReactiveRedisIndexedSessionRepository.RedisSession>,
private val reactiveRedisIndexedSessionRepository: ReactiveRedisIndexedSessionRepository,
private val webSessionStore: SpringSessionWebSessionStore<ReactiveRedisIndexedSessionRepository.RedisSession>,
private val redisSerialiserConfig: RedisSerialiserConfig,
private val springSessionProperties: SpringSessionProperties
) {
private val logger = LoggerFactory.getLogger(SessionControl::class.java)
fun invalidateSessions(username: String): Mono<Void> {
return reactiveSessionRegistry.getAllSessions(username)
.flatMap { session ->
logger.info("Invalidating sessions of user {}", username)
// handle the sessions invalidation process
session.invalidate()
.then(webSessionStore.removeSession(session.sessionId))
.then(Mono.just(session)) // ensure the session object is returned for logging or further processing if needed
}
.then()
.onErrorResume { e ->
logger.error("Error invalidating sessions: ${e.message}")
Mono.empty() // return empty Mono to signify completion even if an error occurred
}
}
fun invalidateSession(session: WebSession): Mono<Void> {
val sessionInformation = createSessionInformation(session)
logger.info("Invalidating sessionId: ${sessionInformation.sessionId}")
// handle the session invalidation process
return sessionInformation.invalidate()
.then(Mono.defer {
webSessionStore.removeSession(sessionInformation.sessionId)
})
.doOnSuccess {
logger.info("Session invalidated and removed: ${sessionInformation.sessionId}")
}
.doOnError { error ->
logger.error("Error invalidating session: ${sessionInformation.sessionId}", error)
}
}
Session Registry
@Component
internal class CustomSpringSessionReactiveSessionRegistry<S : Session>(
reactiveRedisIndexedSessionRepository: ReactiveRedisIndexedSessionRepository,
) : ReactiveSessionRegistry {
private val delegate = SpringSessionBackedReactiveSessionRegistry(
reactiveRedisIndexedSessionRepository,
reactiveRedisIndexedSessionRepository
)
private val logger = LoggerFactory.getLogger(CustomSpringSessionReactiveSessionRegistry::class.java)
override fun getAllSessions(principal: Any?): Flux<ReactiveSessionInformation> {
// custom logic before delegating
logger.info("Custom getAllSessions logic")
return delegate.getAllSessions(principal)
}
override fun saveSessionInformation(information: ReactiveSessionInformation): Mono<Void> {
// custom logic before delegating - not implemented!
logger.info("Custom saveSessionInformation logic")
return delegate.saveSessionInformation(information)
}
override fun getSessionInformation(sessionId: String?): Mono<ReactiveSessionInformation> {
// custom logic before delegating - not implemented properly!
logger.info("Custom getSessionInformation logic")
return delegate.getSessionInformation(sessionId)
}
override fun removeSessionInformation(sessionId: String): Mono<ReactiveSessionInformation> {
// custom logic before delegating - not implemented!
logger.info("Custom removeSessionInformation logic")
return delegate.removeSessionInformation(sessionId)
}
override fun updateLastAccessTime(sessionId: String): Mono<ReactiveSessionInformation> {
// custom logic before delegating - not implemented!
logger.info("Custom updateLastAccessTime logic")
return delegate.updateLastAccessTime(sessionId)
}
}
Session Service
adapted from https://docs.spring.io/spring-session/reference/configuration/redis.html#finding-all-user-sessions
@Service
internal class SessionService(
private val sessions: ReactiveFindByIndexNameSessionRepository<RedisSession>,
private val redisIndexedSessionRepository: ReactiveRedisIndexedSessionRepository
) {
private val logger = LoggerFactory.getLogger(SessionService::class.java)
/**
* Retrieves all sessions for a specific user.
* @param principal the principal whose sessions need to be retrieved
* @return a Flux of sessions for the specified user
*/
fun getSessions(principal: Principal): Flux<Session> {
logger.info("Getting all sessions for: ${principal.name}")
return sessions.findByPrincipalName(principal.name)
.flatMapMany { sessionsMap ->
Flux.fromIterable(sessionsMap.values)
}
}
/**
* Removes a specific session for a user.
* @param principal the principal whose session needs to be removed
* @param sessionIdToDelete the ID of the session to be removed
* @return a Mono indicating completion or error
*/
fun removeSession(principal: Principal, sessionIdToDelete: String): Mono<Void> {
logger.info("Removing session for: ${principal.name}, with session id: $sessionIdToDelete")
return sessions.findByPrincipalName(principal.name)
.flatMap { userSessions ->
if (userSessions.containsKey(sessionIdToDelete)) {
redisIndexedSessionRepository.deleteById(sessionIdToDelete)
} else {
Mono.empty()
}
}
}
}
Problems I currently have:
SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (securityContext != null && securityContext.getAuthentication() != null) {
return securityContext.getAuthentication().getName();
}
My security context is stored as an attribute in Redis, in the session, so the above, the received attribute cannot be cast directly to SecurityContext object, without going via a de-serialiser. Unless I'm doing something fundamentally wrong in the code.
I can share the full github repo if you need it.
closing issue. I've better explained it in another post
How do I implement Session Control, when I use @EnableRedisWebSession on my security chain? Spring Session automatically creates beans that implement @WebSession and @WebsessionStore But the function below, in order to autowire WebSessionStore, needs to see a manual implementation of WebsessionStore, so what do I do?
Describe the bug A clear and concise description of what the bug is.
To Reproduce Steps to reproduce the behavior.
Expected behavior A clear and concise description of what you expected to happen.
Sample
A link to a GitHub repository with a minimal, reproducible sample.
Reports that include a sample will take priority over reports that do not. At times, we may require a sample, so it is good to try and include a sample up front.