如何解决 SpringSessionBackedReactiveSessionRegistry 错误? (春季安全,春季会议)

问题描述 投票:0回答:1

有关如何解决以下错误的任何建议,或者我是否做了一些根本错误的事情?每当我执行类似 val contextAttr = session.getAttribute>(springAttribute) 之类的操作时,contextAttr 肯定会以 LinkedHashedMap 的形式返回。这些对象作为主会话的属性存储在 Redis 中,我相信这些都在 JSON / Map 中 格式。

描述错误

当我的 Spring 注销处理程序调用此函数时

fun invalidateSession(sessionId: String): Mono<Void> {
        logger.info("Invalidating sessionId: ${sessionId}")
        // handle the session invalidation process
        return reactiveSessionRegistry.getSessionInformation(sessionId)
            .flatMap { session ->
                // invalidate session
                session.invalidate()
                    .then(
                        // delete session
                        webSessionStore.removeSession(sessionId)
                    )
                    .doOnSuccess {
                        logger.info("Session invalidated and removed: ${sessionId}")
                    }
                    .doOnError { error ->
                        logger.error("Error invalidating session: ${sessionId}", error)
                    }
            }
    }

SpringSessionBackedReactiveSessionRegistry 中的以下函数被调用:

    @Override
    public Mono<ReactiveSessionInformation> getSessionInformation(String sessionId) {
        return this.sessionRepository.findById(sessionId).map(SpringSessionBackedReactiveSessionInformation::new);
    }

内部类实现如下:

class SpringSessionBackedReactiveSessionInformation extends ReactiveSessionInformation {

        SpringSessionBackedReactiveSessionInformation(S session) {
            super(resolvePrincipalName(session), session.getId(), session.getLastAccessedTime());
        }

        private static String resolvePrincipalName(Session session) {
            String principalName = session
                .getAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
            if (principalName != null) {
                return principalName;
            }
            SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT);
            if (securityContext != null && securityContext.getAuthentication() != null) {
                return securityContext.getAuthentication().getName();
            }
            return "";
        }

        @Override
        public Mono<Void> invalidate() {
            return super.invalidate()
                .then(Mono.defer(() -> SpringSessionBackedReactiveSessionRegistry.this.sessionRepository
                    .deleteById(getSessionId())));
        }

    }

但是,在这一行:

SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT);

SPRING_SECURITY_CONTEXT 是从 Redis 作为 HashMap 或 LinkedHashMap 接收的,因此无法转换为 SecurityContext(无需正确的反序列化)

这就是完全我看到的错误:

enter image description here

两次调用才能获取会话?

另外,我不确定这是否再次调用 Redis 来获取安全上下文,但有必要吗?在调用 /logout 端点之前给出,无论如何都会检索会话/安全上下文,(见下文。)

SessionId 将来自会话,在这里,当一开始调用它时 fun invalidateSession(sessionId: String): Mono ),因此调用 getSessionInformation(String sessionId) 并使用它,再次调用 this.sessionRepository.findById(sessionId) ,好像有点浪费……?

重现 参见上文,只需尝试上述操作,将会话存储到 redis,然后尝试调用上述函数使会话失效

预期行为 应正确反序列化铸造。 linkedHashmap 不能直接转换为 SecurityContext 对象

样品

见上文。 Github 代码可以在这里找到:

我的实现 https://github.com/dreamstar-enterprises/docs/blob/master/Spring%20BFF/bff/auth/sessions/SessionControl.kt https://github.com/dreamstar-enterprises/docs/blob/master/Spring%20BFF/bff/auth/sessions/SessionRegistryConfig.kt

Spring 实现(我相信其中有错误) https://github.com/spring-projects/spring-session/blob/main/spring-session-core/src/main/java/org/springframework/session/security/SpringSessionBackedReactiveSessionRegistry.java

感谢任何帮助或建议!

spring-security redis spring-session
1个回答
0
投票

这是使用会话序列化时的一个常见问题,尤其是使用 Redis,因为存储的对象通常会序列化为 JSON 或类似的格式,在检索时,这些对象可能会变成 Map 而不是预期的对象类型。我在我的组织中的另一个 spring 项目中遇到了类似的问题,我必须编写自定义序列化/反序列化,所以这里的代码我认为也可以适用于您的情况

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import org.springframework.data.redis.serializer.RedisSerializer
import org.springframework.data.redis.serializer.SerializationException
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextImpl
import org.springframework.security.core.Authentication
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken

class SecurityContextSerializer : RedisSerializer<SecurityContext> {

    private val objectMapper: ObjectMapper = ObjectMapper().registerModule(KotlinModule())

    override fun serialize(securityContext: SecurityContext?): ByteArray? {
        return try {
            // Convert SecurityContext to JSON bytes
            objectMapper.writeValueAsBytes(securityContext)
        } catch (e: Exception) {
            throw SerializationException("Error serializing SecurityContext", e)
        }
    }

    override fun deserialize(bytes: ByteArray?): SecurityContext? {
        return try {
            if (bytes == null || bytes.isEmpty()) {
                return null
            }
            // Convert JSON bytes back to SecurityContext
            val map: Map<String, Any> = objectMapper.readValue(bytes)
            mapToSecurityContext(map)
        } catch (e: Exception) {
            throw SerializationException("Error deserializing SecurityContext", e)
        }
    }

    private fun mapToSecurityContext(map: Map<String, Any>): SecurityContext {
        // Custom logic to convert map back to SecurityContext
        val authMap = map["authentication"] as? Map<String, Any>
        val authentication = authMap?.let { mapToAuthentication(it) }
        return SecurityContextImpl(authentication)
    }

    private fun mapToAuthentication(map: Map<String, Any>): Authentication {
        // Custom logic to convert map to Authentication, this example uses UsernamePasswordAuthenticationToken
        val principal = map["principal"]
        val credentials = map["credentials"]
        val authorities = emptyList<Any>() // Implement authority conversion if needed
        return UsernamePasswordAuthenticationToken(principal, credentials, authorities)
    }
}

您必须确保 Spring 在读取和写入会话数据时使用您的自定义序列化程序。

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession
import org.springframework.session.data.redis.ReactiveRedisSessionRepository

@Configuration
@EnableRedisWebSession
class RedisConfig {

    @Bean
    fun reactiveRedisSessionRepository(factory: LettuceConnectionFactory): ReactiveRedisSessionRepository {
        val redisTemplate = RedisTemplate<String, SecurityContext>()
        redisTemplate.setConnectionFactory(factory)
        redisTemplate.valueSerializer = SecurityContextSerializer()
        redisTemplate.afterPropertiesSet()
        return ReactiveRedisSessionRepository(redisTemplate)
    }
}

另外,

避免冗余的 Redis 调用:正如您所指出的,调用

sessionRepository.findById(sessionId)
两次是多余的,特别是因为您已经有了会话信息。您可以将会话对象直接传递给失效方法,而不用再次检索会话,从而避免不必要的 Redis 调用。 您可以修改代码以直接使用会话对象,而不用再次调用
getSessionInformation(sessionId)

fun invalidateSession(session: ReactiveSessionInformation): Mono<Void> {
    logger.info("Invalidating sessionId: ${session.getSessionId()}")
    return session.invalidate()
        .then(
            // delete session
            webSessionStore.removeSession(session.getSessionId())
        )
        .doOnSuccess {
            logger.info("Session invalidated and removed: ${session.getSessionId()}")
        }
        .doOnError { error ->
            logger.error("Error invalidating session: ${session.getSessionId()}", error)
        }
}

修改

resolvePrincipalName
以处理映射:由于
SecurityContext
正在作为映射检索,因此修改
resolvePrincipalName
函数以检测此情况并相应地处理反序列化:

private static String resolvePrincipalName(Session session) {
    Object principalNameObj = session.getAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
    if (principalNameObj instanceof String) {
        return (String) principalNameObj;
    }
    Object securityContextObj = session.getAttribute(SPRING_SECURITY_CONTEXT);
    if (securityContextObj instanceof LinkedHashMap) {
        // Deserialize LinkedHashMap to SecurityContext here
        SecurityContext securityContext = deserializeSecurityContext((LinkedHashMap) securityContextObj);
        if (securityContext != null && securityContext.getAuthentication() != null) {
            return securityContext.getAuthentication().getName();
        }
    }
    return "";
}

private static SecurityContext deserializeSecurityContext(LinkedHashMap map) {
    // Custom logic to convert the map back to SecurityContext
}
© www.soinside.com 2019 - 2024. All rights reserved.