有关如何解决以下错误的任何建议,或者我是否做了一些根本错误的事情?每当我执行类似 val contextAttr = session.getAttribute
描述错误
当我的 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(无需正确的反序列化)
这就是完全我看到的错误:
两次调用才能获取会话?
另外,我不确定这是否再次调用 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
感谢任何帮助或建议!
这是使用会话序列化时的一个常见问题,尤其是使用 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
}