spring-projects / spring-session

Spring Session
https://spring.io/projects/spring-session
Apache License 2.0
1.86k stars 1.11k forks source link

Session Control with Redis #3113

Closed dreamstar-enterprises closed 3 weeks ago

dreamstar-enterprises commented 1 month ago

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?

@Component
internal class SessionControl(
    private val reactiveSessionRegistry: SpringSessionBackedReactiveSessionRegistry<ReactiveRedisIndexedSessionRepository.RedisSession>,
    private val webSessionStore: WebSessionStore
) {

    fun invalidateSessions(username: String): Mono<Void> {
        return reactiveSessionRegistry.getAllSessions(username)
            .flatMap { session ->
                session.invalidate() // invalidate the session
                    .then(webSessionStore.removeSession(session.sessionId)) // remove from WebSessionStore
                    .then(Mono.just(session)) // ensure the session object is returned for logging or further processing if needed
            }
            .then() // complete the Mono after processing all sessions
    }
}

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.

marcusdacoregio commented 1 month 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?

dreamstar-enterprises commented 1 month ago

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

dreamstar-enterprises commented 3 weeks ago

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.

dreamstar-enterprises commented 3 weeks ago

closing issue. I've better explained it in another post