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
相关产品推荐
相关产品推荐

