Spring Security Session 序列化策略分析:为什么不是 JSON?
一、前言
在分布式 session 管理中,序列化是核心环节。本文深入分析 Spring Security/Spring Session 的序列化策略选择逻辑
二、Spring Session 序列化演进史
| 版本 | 默认序列化器 | 特点 |
|---|---|---|
| Spring Session 1.x | JDK 序列化 | 简单直接 |
| Spring Boot 1.x | JDK 序列化 | 兼容性优先 |
| Spring Boot 2.x | Jackson (JSON) | 性能+可读性 |
| Spring Boot 3.x + Spring Session 2.x | Jackson/Kryo | 多选择 |
三、Spring Security 不默认用 JSON 的深层原因
3.1 安全性考量
java
// JDK 序列化存在反序列化漏洞风险
// Spring Security 需要处理 Authentication 对象
public interface Authentication extends Serializable {
Object getPrincipal();
Object getCredentials();
Collection<? extends GrantedAuthority> getAuthorities();
}
问题场景:
java
// 如果使用 JSON,Authentication 的 credentials 可能包含敏感信息
{
"principal": "user",
"credentials": "password123", // 敏感!
"authenticated": true
}
Spring Security 的策略:
CredentialsContainer接口强制在序列化前清除凭证- JDK 序列化可以控制哪些字段被序列化
3.2 类型完整性
Authentication 接口体系:
├── AbstractAuthenticationToken
│ └── UsernamePasswordAuthenticationToken
├── TestingAuthenticationToken
├── RememberMeAuthenticationToken
└── 自定义实现(如 ProxyAuthenticationToken)
JSON 序列化痛点:
java
// 多态类型反序列化问题
ObjectMapper mapper = new ObjectMapper();
mapper.readValue(json, Authentication.class);
// ❌ 无法正确反序列化为具体子类型
// 必须配置:
mapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.allowIfSubType(Object.class)
.build(),
ObjectMapper.DefaultTyping.NON_FINAL,
"@class"
);
3.3 性能与兼容性权衡
| 指标 | JDK 序列化 | JSON | Kryo |
|---|---|---|---|
| 序列化速度 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 序列化大小 | 大 | 中 | 小 |
| 反序列化速度 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 安全性 | 低 | 高 | 中 |
| 兼容性 | 严格 | 灵活 | 中 |
Spring 的选择逻辑:
- Spring Boot 1.x: JDK 序列化,兼容性优先(当时 JSON 库版本混乱)
- Spring Boot 2.x: 切换 Jackson,性能+生态成熟
- Spring Session 2.x: 多种序列化器可选,开发者自行选择
3.4 Session 数据特点
java
// Spring Session 存储的典型数据
Session session = new MapSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
// securityContext.authentication 是核心
Session 数据的特点:
- 读多写少:session 创建后读取频繁
- 对象复杂:嵌套 Authentication、GrantedAuthority 集合
- 安全敏感:principal/credentials 需要特殊处理
- 生命周期长:可能存在数小时甚至数天
3.5 Spring Session 的默认策略
java
// Spring Session Redis 配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {
// Spring Boot 自动配置会使用:
// 1. JacksonJsonRedisSerializer (Spring Boot 2.x)
// 2. 或开发者自定义的序列化器
}
开发者可以自定义:
java
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
// Kryo - 性能最优
return new KryoRedisSerializer<>(Object.class);
// 或 Hessian - 跨语言支持
return new HessianRedisSerializer<>(Object.class);
// 或自定义 Jackson - 可读性好
return new Jackson2JsonRedisSerializer<>(Object.class);
}
四、实际项目中的最佳实践
4.1 推荐方案:Kryo 序列化
xml
<!-- pom.xml -->
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.0</version>
</dependency>
java
@Configuration
public class KryoSessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new KryoRedisSerializer<>();
}
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return new CookieHttpSessionIdResolver();
}
}
java
// KryoRedisSerializer 实现
public class KryoRedisSerializer<T> implements RedisSerializer<T> {
private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
// 不要求必须存在无参构造函数
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
kryo.setRegistrationRequired(false);
// 注册 Security 相关类
kryo.register(Authentication.class);
kryo.register(GrantedAuthority.class);
kryo.register(AbstractAuthenticationToken.class);
return kryo;
});
@Override
public byte[] serialize(T obj) {
if (obj == null) return null;
Kryo kryo = kryoThreadLocal.get();
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output output = new Output(baos)) {
kryo.writeClassAndObject(output, obj);
output.flush();
return baos.toByteArray();
} catch (IOException e) {
throw new SerializationException("kryo serialize error", e);
}
}
@Override
public T deserialize(byte[] bytes) {
if (bytes == null || bytes.length == 0) return null;
Kryo kryo = kryoThreadLocal.get();
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Input input = new Input(bais)) {
return (T) kryo.readClassAndObject(input);
} catch (Exception e) {
throw new KryoSerializationException("序列化失败");
}
}
}
4.2 确保 Token 类可序列化
4.3 配置说明
yaml
spring:
session:
store-type: redis
redis:
namespace: spring:session
# 配置超时时间
timeout: 30m
五、总结:为什么不是 JSON?
| 考量维度 | JDK | JSON | Kryo | Spring 选择 |
|---|---|---|---|---|
| 安全性 | ❌ | ✅ | ⚠️ | 历史原因 |
| 类型完整性 | ✅ | ⚠️ | ✅ | |
| 性能 | ❌ | ⚠️ | ✅ | |
| 可读性 | ❌ | ✅ | ❌ | |
| 跨语言 | ❌ | ✅ | ⚠️ | |
| Spring 生态 | 早期默认 | Spring Boot 2.x+ | 可选 | 演进选择 |
核心结论:
- 历史原因:Spring 1.x/2.x 早期,JSON 库生态不成熟
- 安全性:Spring Security 优先考虑安全性,JDK 序列化更可控
- 类型系统:JDK 序列化能保持完整的类型信息
- 当前最佳实践:Spring Boot 2.x+ 推荐使用 Jackson 或 Kryo
最终建议:
- Spring Boot 2.x+ 应用:使用默认 Jackson 或切换到 Kryo
- 高性能场景:使用 Kryo
- 跨语言场景:使用 Hessian 或 JSON
参考文档: