You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Session集成Redis后Principal丢失问题求助

排查Spring Session Redis下Principal偶尔丢失的问题

这种偶发的会话异常确实挺闹心的——明明还能访问受限页面,但关键的Principal却为空,直接影响业务逻辑。结合Spring Session + Redis的常见坑,给你几个排查和解决的方向:

1. 会话序列化/反序列化异常

Spring Session默认的JDK序列化方式有时候会踩坑:如果你的用户类(或者Principal关联的对象)没正确实现Serializable接口,或者序列化过程中某些属性被遗漏,就会导致Redis中存储的会话数据反序列化后Principal为空,但Spring Security的权限缓存还没失效,所以你还能访问受限页面。

解决方案:换成JSON序列化方式,更稳定也更易调试。你可以在配置类里自定义序列化器:

@Configuration
public class RedisSessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        return serializer;
    }
}

或者直接用配置文件简化配置:

spring.session.data.redis.serializer.type=json

2. 会话过期时间不一致

如果Redis中存储的会话键过期时间,和Spring Boot配置的会话超时时间不匹配,就可能出现:Redis里的会话已经被清理,但Spring Security的权限缓存还没过期,导致你能访问页面,但会话里的Principal已经没了。

排查步骤

  • 先确认你的会话超时配置:
server.servlet.session.timeout=3600s # 比如1小时
  • 用Redis客户端执行KEYS spring:session:*找到会话键,再用TTL 键名查看过期时间,确保和配置的超时时间一致。
  • 如果不一致,检查是否设置了Redis的默认过期策略,或者Spring Session的命名空间是否正确:
spring.session.data.redis.namespace=spring:session

3. 并发请求的竞态条件

当多个请求同时操作会话时,可能出现竞态问题:比如一个请求在刷新会话数据,另一个请求同时读取,导致读到未完全更新的会话,Principal就成了null。

解决方案

  • 调整Spring Session的Redis刷新模式为IMMEDIATE,确保会话修改后立即写入Redis:
spring.session.data.redis.flush-mode=immediate

不过这个模式会增加Redis的请求量,需要权衡性能;

  • 也可以尝试开启Redis事务支持,减少并发冲突的概率。

4. Spring Security与Session的上下文同步问题

如果你的系统里有异步方法,或者SecurityContext的传递有问题,就可能出现:SecurityContextHolder里的权限信息还在,但Session中的Principal已经丢失,导致能访问页面但获取不到用户信息。

解决方案

  • 如果有异步方法,确保异步线程能正确继承SecurityContext:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(runnable -> {
            // 保存当前线程的SecurityContext
            SecurityContext context = SecurityContextHolder.getContext();
            return () -> {
                try {
                    // 给异步线程设置上下文
                    SecurityContextHolder.setContext(context);
                    runnable.run();
                } finally {
                    SecurityContextHolder.clearContext();
                }
            };
        });
        executor.initialize();
        return executor;
    }
}
  • 检查Spring Security的会话创建策略,确保是基于Session的:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
    }
}

5. Redis连接或网络波动

偶尔的Redis连接超时、网络抖动,可能导致会话数据没有正确写入或读取。当读取会话失败时,Spring Session可能会创建一个空的新会话,但Spring Security的权限缓存还没失效,就会出现你遇到的情况。

排查步骤

  • 查看Redis的日志,是否有连接超时、读写错误的记录;
  • 调整Redis连接池的配置,增加超时时间和重试机制:
spring.redis.timeout=5000ms
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-wait=-1ms

建议你先从序列化问题会话过期时间入手排查,这两个是最常见的原因。如果还是没解决,可以尝试在出现问题时,用Redis客户端直接查看会话数据,看看里面的Principal是否真的为空,这样能更快定位问题。

内容的提问来源于stack exchange,提问作者ROZZ

火山引擎 最新活动