基于Spring Boot 2 WebFlux(Netty)的无后端存储会话管理咨询
刚好做过类似的需求!在Spring Boot 2 + WebFlux(Netty)环境下,要实现Play框架那样的客户端会话(会话数据存在Cookie中,随请求携带),完全可以通过自定义WebSessionManager来实现,全程不需要依赖数据库/Redis,也不用开粘性会话。下面是具体的实现方案:
核心思路
把会话数据(比如用户ID、权限标识等)序列化为JSON,加密后存储在HTTP Cookie中;每个请求到达时,服务端从Cookie读取加密数据,解密、反序列化得到会话对象;响应时再把更新后的会话数据加密写回Cookie。这样所有节点都能独立处理请求,无需粘性会话,也不用后端存储。
具体实现步骤
1. 配置加密Bean(确保会话数据安全)
首先需要一个安全的加密工具,用来加密/解密会话数据。这里用Spring Security的AesBytesEncryptor(Spring Boot WebFlux项目一般已间接依赖spring-security-core):
@Configuration public class EncryptionConfig { @Bean public BytesEncryptor cookieSessionEncryptor(@Value("${session.cookie.secret-key}") String secretKey) { // 生产环境:密钥必须从安全渠道获取(比如环境变量、密钥管理服务),绝对不能硬编码! // 密钥需要是256位(32字节),如果用字符串要确保长度足够 return new AesBytesEncryptor(secretKey, KeyGenerators.string().generateKey()); } }
2. 自定义WebSession实现
实现WebSession接口,封装会话数据,同时支持序列化/反序列化:
public class CookieWebSession implements WebSession { private final String id; private final Map<String, Object> attributes = new ConcurrentHashMap<>(); private Instant creationTime = Instant.now(); private Instant lastAccessTime = Instant.now(); private boolean started = true; private boolean expired = false; private Duration maxIdleTime = Duration.ofMinutes(30); // 构造方法:生成唯一会话ID public CookieWebSession() { this.id = UUID.randomUUID().toString(); } // 实现WebSession的关键方法 @Override public String getId() { return id; } @Override public <T> T getAttribute(String name) { return (T) attributes.get(name); } @Override public void setAttribute(String name, Object value) { attributes.put(name, value); } @Override public boolean isExpired() { if (expired) return true; // 检查是否超过最大空闲时间 return lastAccessTime.plus(maxIdleTime).isBefore(Instant.now()); } @Override public void setLastAccessTime(Instant lastAccessTime) { this.lastAccessTime = lastAccessTime; } // 其他方法(比如invalidate、getMaxIdleTime等)按需实现 }
3. 自定义WebSessionManager(核心逻辑)
这个类负责从Cookie读取/写入会话数据,处理加密、解密、序列化逻辑:
@Component public class CookieWebSessionManager implements WebSessionManager { private static final String SESSION_COOKIE_NAME = "WEBFLUX_CLIENT_SESSION"; private final BytesEncryptor encryptor; private final ObjectMapper objectMapper; // 构造注入加密器和Jackson的ObjectMapper public CookieWebSessionManager(BytesEncryptor encryptor, ObjectMapper objectMapper) { this.encryptor = encryptor; this.objectMapper = objectMapper; } @Override public Mono<WebSession> getSession(ServerWebExchange exchange) { // 尝试从请求Cookie中获取会话数据 return Mono.justOrEmpty(exchange.getRequest().getCookies().getFirst(SESSION_COOKIE_NAME)) .map(Cookie::getValue) .flatMap(this::decryptAndDeserializeSession) // 如果Cookie不存在或解析失败,创建新会话 .switchIfEmpty(Mono.defer(this::createNewSession)) .doOnNext(session -> { // 更新会话最后访问时间 session.setLastAccessTime(Instant.now()); // 将更新后的会话加密后写回响应Cookie serializeAndEncryptSession(session) .map(encryptedValue -> buildSessionCookie(encryptedValue, exchange)) .doOnNext(cookie -> exchange.getResponse().addCookie(cookie)) .subscribe(); }); } // 解密并反序列化会话数据 private Mono<CookieWebSession> decryptAndDeserializeSession(String encryptedValue) { try { byte[] decryptedBytes = encryptor.decrypt(Base64.getDecoder().decode(encryptedValue)); String sessionJson = new String(decryptedBytes, StandardCharsets.UTF_8); CookieWebSession session = objectMapper.readValue(sessionJson, CookieWebSession.class); // 如果会话已过期,创建新会话 return session.isExpired() ? createNewSession() : Mono.just(session); } catch (Exception e) { // 解密/反序列化失败(比如数据篡改、密钥变更),直接返回新会话 return createNewSession(); } } // 序列化并加密会话数据 private Mono<String> serializeAndEncryptSession(CookieWebSession session) { try { String sessionJson = objectMapper.writeValueAsString(session); byte[] encryptedBytes = encryptor.encrypt(sessionJson.getBytes(StandardCharsets.UTF_8)); return Mono.just(Base64.getEncoder().encodeToString(encryptedBytes)); } catch (JsonProcessingException e) { return Mono.error(e); } } // 创建新会话 private Mono<CookieWebSession> createNewSession() { return Mono.just(new CookieWebSession()); } // 构建响应Cookie(安全配置很重要!) private ResponseCookie buildSessionCookie(String value, ServerWebExchange exchange) { return ResponseCookie.from(SESSION_COOKIE_NAME, value) .httpOnly(true) // 禁止JS读取,防范XSS攻击 .secure(exchange.getRequest().isSecure()) // HTTPS环境下必须开启,防止Cookie被明文传输 .path("/") // 全局生效 .maxAge(Duration.ofMinutes(30)) // 和会话最大空闲时间保持一致 .sameSite(SameSite.LAX) // 防范CSRF攻击,根据业务需求选择Strict/LAX/NONE .build(); } }
4. 配置WebFlux使用自定义SessionManager
最后,让WebFlux使用我们的自定义CookieWebSessionManager:
@Configuration public class WebFluxSessionConfig implements WebFluxConfigurer { private final CookieWebSessionManager cookieWebSessionManager; public WebFluxSessionConfig(CookieWebSessionManager cookieWebSessionManager) { this.cookieWebSessionManager = cookieWebSessionManager; } @Override public void configureWebSessionManager(WebSessionManagerConfigurer configurer) { configurer.sessionManager(cookieWebSessionManager); } }
关键安全与性能提示
- Cookie安全配置:必须开启
httpOnly、secure(HTTPS环境)、SameSite,这是防范XSS、CSRF和明文传输的基础。 - 密钥安全:绝对不能硬编码密钥,生产环境要通过环境变量、Spring Cloud Config或专业密钥管理服务(比如AWS KMS)获取。
- 会话数据大小:Cookie有4KB左右的大小限制,所以只能存储必要的信息(比如用户ID、权限标识),不要存大量业务数据。
- 加密算法选择:建议用AES-256-GCM(
AesBytesEncryptor默认就是这个),兼具安全性和性能。 - 会话过期处理:每次请求都会更新Cookie的过期时间,确保用户活跃时会话不会失效。
总结
这样实现后,你就可以在控制器里正常使用WebSession了,和普通会话用法完全一致,但会话数据是存在客户端Cookie中的,不需要后端存储,也不用配置粘性会话,完美匹配你的需求。
内容的提问来源于stack exchange,提问作者mahanhz
相关产品推荐
相关产品推荐

