You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Session自定义会话内容:限定Redis存储会话数据的方案咨询

这确实是个很典型的混合技术栈场景问题——既要借助Spring Session+Redis实现分布式会话共享,又不想把JSF ViewScope里那些又大又没必要跨节点的控制器对象塞进Redis,对吧?我来给你几个可行的方案,从简单的属性过滤到更优雅的数据分离都有:

方案1:自定义Redis会话仓库,过滤存储属性

Spring Session的核心是通过RedisIndexedSessionRepository来持久化会话数据,我们可以继承这个类,在会话保存到Redis前临时移除不需要的属性,保存后再恢复,这样既保证Redis只存认证数据,又不影响JSF对本地会话属性的访问。

代码示例:

@Component
public class FilteredRedisSessionRepository extends RedisIndexedSessionRepository {

    // Spring Security认证数据的默认键前缀
    private static final String SECURITY_ATTR_PREFIX = "SPRING_SECURITY_CONTEXT";

    public FilteredRedisSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
        super(sessionRedisOperations);
    }

    @Override
    public void save(Session session) {
        // 1. 先备份会话中所有属性
        Map<String, Object> allAttributes = new HashMap<>();
        session.getAttributeNames().forEach(attrName -> 
            allAttributes.put(attrName, session.getAttribute(attrName))
        );

        // 2. 清空会话原有属性,只保留Spring Security相关数据
        session.getAttributeNames().forEach(session::removeAttribute);
        allAttributes.entrySet().stream()
            .filter(entry -> entry.getKey().startsWith(SECURITY_ATTR_PREFIX))
            .forEach(entry -> session.setAttribute(entry.getKey(), entry.getValue()));

        // 3. 执行父类的保存逻辑,此时只有认证数据会写入Redis
        super.save(session);

        // 4. 把过滤掉的属性重新放回会话,保证JSF能正常访问
        allAttributes.entrySet().stream()
            .filter(entry -> !entry.getKey().startsWith(SECURITY_ATTR_PREFIX))
            .forEach(entry -> session.setAttribute(entry.getKey(), entry.getValue()));
    }
}

这个方案的优势是改动小,不需要调整现有会话逻辑,只是在持久化环节做了拦截过滤。

方案2:自定义会话属性序列化器

另一种思路是通过自定义SessionAttributeSerializer,在序列化阶段只处理需要共享的属性,直接跳过JSF的控制器对象。

代码示例:

public class SecurityOnlySessionSerializer implements SessionAttributeSerializer {

    // 用默认的JDK序列化器做委托
    private final SessionAttributeSerializer delegate = new JdkSerializationSessionAttributeSerializer();

    @Override
    public void serialize(Map<String, Object> attributes, OutputStream outputStream) throws IOException {
        // 只保留Spring Security认证属性
        Map<String, Object> filteredAttrs = attributes.entrySet().stream()
            .filter(entry -> entry.getKey().equals("SPRING_SECURITY_CONTEXT"))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        
        delegate.serialize(filteredAttrs, outputStream);
    }

    @Override
    public Map<String, Object> deserialize(InputStream inputStream) throws IOException {
        // 反序列化时只拿到认证数据,其他属性会在本地会话中重新生成
        return delegate.deserialize(inputStream);
    }
}

然后在Spring Session配置类中注册这个序列化器:

@Configuration
@EnableRedisHttpSession
public class SpringSessionConfig {

    @Bean
    public RedisIndexedSessionRepository redisSessionRepository(RedisConnectionFactory connectionFactory) {
        RedisIndexedSessionRepository repository = new RedisIndexedSessionRepository(connectionFactory);
        repository.setDefaultSerializer(new SecurityOnlySessionSerializer());
        return repository;
    }
}

这个方案更聚焦于序列化环节,适合对会话存储逻辑不想做太多侵入的场景。

方案3:分离共享数据与本地会话(推荐)

如果觉得上面的方案有点“hack”,更优雅的方式是把跨节点共享的认证数据和本地专属的JSF ViewScope数据彻底分开:

  • 让Spring Session+Redis只负责存储Spring Security的认证信息,用自定义SecurityContextRepository单独管理;
  • JSF的ViewScope控制器仍然保留在应用服务器的本地HttpSession中(本来ViewScope就和当前视图绑定,不需要跨节点共享)。

代码示例(自定义SecurityContextRepository):

@Component
public class RedisSecurityContextRepository implements SecurityContextRepository {

    private final StringRedisTemplate redisTemplate;
    private static final String SECURITY_CONTEXT_KEY = "security:context:%s";
    // 会话过期时间和本地Session保持一致
    private static final int SESSION_TIMEOUT_MINUTES = 30;

    public RedisSecurityContextRepository(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        HttpServletRequest request = requestResponseHolder.getRequest();
        String sessionId = request.getSession().getId();
        String contextJson = redisTemplate.opsForValue().get(String.format(SECURITY_CONTEXT_KEY, sessionId));
        
        if (contextJson != null) {
            // 用Jackson反序列化SecurityContext,需确保UserDetails可序列化
            try {
                return new ObjectMapper().readValue(contextJson, SecurityContextImpl.class);
            } catch (JsonProcessingException e) {
                return new SecurityContextImpl();
            }
        }
        return new SecurityContextImpl();
    }

    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        String sessionId = request.getSession().getId();
        try {
            String contextJson = new ObjectMapper().writeValueAsString(context);
            redisTemplate.opsForValue().set(
                String.format(SECURITY_CONTEXT_KEY, sessionId),
                contextJson,
                SESSION_TIMEOUT_MINUTES,
                TimeUnit.MINUTES
            );
        } catch (JsonProcessingException e) {
            // 处理序列化异常
        }
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        return redisTemplate.hasKey(String.format(SECURITY_CONTEXT_KEY, sessionId));
    }
}

然后在Spring Security配置中启用这个仓库:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private RedisSecurityContextRepository securityContextRepository;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityContext(config -> config.securityContextRepository(securityContextRepository))
            // 其他Security配置...
        return http.build();
    }
}

这种方案的优势是完全解耦了共享数据和本地会话,Redis里只存必要的认证信息,避免了大对象序列化的性能问题,也更符合ViewScope的设计初衷。

注意事项

  • 确保JSF的ViewScope控制器不需要跨节点访问(ViewScope本来就是针对单个视图请求的,跨节点访问没有意义);
  • 如果自定义了UserDetails,要保证它是可序列化的(或者用Jackson替代JDK序列化);
  • 同步Redis中认证数据的过期时间和本地Session的过期时间,避免出现认证信息残留的情况。

内容的提问来源于stack exchange,提问作者Michał Budziło

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 15:12:30