序列化安全与替代方案
Java 原生序列化使用方便,但在生产环境尤其是服务端系统中,安全风险很高。理解这些风险,比会写 ObjectOutputStream 更重要。
# 1. 反序列化为什么危险
反序列化不是简单地把字节变成字段值。Java 在恢复对象时可能触发类加载、对象构造相关逻辑、readObject、readResolve 等机制。
如果应用反序列化了不可信数据,攻击者可能构造恶意字节流,利用 classpath 中某些类的调用链执行危险操作。
这类问题常被称为反序列化漏洞。
# 2. 高风险写法
try (ObjectInputStream input = new ObjectInputStream(request.getInputStream())) {
Object obj = input.readObject();
}
如果 request.getInputStream() 来自外部用户,这种写法非常危险。
# 3. 基本防护原则
- 不反序列化不可信输入。
- 不把 Java 原生序列化暴露在公网接口。
- 限制可反序列化的类型白名单。
- 升级依赖,减少已知 gadget 链。
- 使用 JDK 提供的对象输入过滤机制。
- 优先选择更安全、可控的序列化格式。
# 4. ObjectInputFilter
Java 提供了 ObjectInputFilter 用于限制反序列化。
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;java.base/*;!*"
);
try (ObjectInputStream input = new ObjectInputStream(source)) {
input.setObjectInputFilter(filter);
Object obj = input.readObject();
}
过滤规则可以限制类名、数组大小、对象图深度、引用数量等。即使使用过滤,也不意味着可以放心处理任意不可信数据,它只是防线之一。
# 5. 版本兼容问题
原生序列化还存在长期兼容问题:
- 类名变化会影响反序列化。
- 包名变化会影响反序列化。
- 字段类型变化可能不兼容。
serialVersionUID不一致会失败。- 对象图结构复杂时演进困难。
这使它不适合作为长期存储格式或跨服务协议。
# 6. 替代方案
# 6.1 JSON
优点:
- 可读性好。
- 调试方便。
- 生态成熟。
- 适合 HTTP API 和配置文件。
缺点:
- 体积较大。
- 性能通常不如二进制协议。
- Schema 约束较弱。
# 6.2 Protocol Buffers
优点:
- 跨语言。
- 性能好。
- 体积小。
- Schema 清晰。
- 适合 RPC 和服务间通信。
缺点:
- 可读性不如 JSON。
- 需要维护
.proto文件。
# 6.3 Avro
适合大数据和消息系统,Schema 演进能力较好。
# 6.4 Kryo
性能较好,常见于一些 Java 生态内部序列化场景,但跨语言和长期兼容要谨慎评估。
# 7. 选型建议
- 对外 API:JSON。
- 内部高性能 RPC:Protocol Buffers。
- 大数据链路:Avro、Parquet 等生态格式。
- Java 本地临时缓存:可评估 Kryo,但要控制类型和版本。
- 不可信输入:不要使用 Java 原生反序列化。
# 专家实践清单
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 外部 API | JSON | 可读、生态好 |
| 高性能 RPC | Protobuf / Thrift | Schema 明确、跨语言 |
| 大数据存储 | Avro / Parquet | 生态成熟、可演进 |
| Java 本地缓存 | 谨慎评估 Kryo | 性能好但版本要控 |
| 不可信输入 | 禁止原生反序列化 | 安全风险高 |
安全边界模型:
外部字节
│ 不可信
▼
类型过滤 / 大小限制 / 深度限制
│
▼
反序列化
│
▼
业务对象
# Tips 快问快答
Q:为什么 Java 原生序列化安全风险高? A:反序列化会创建对象并触发对象链逻辑,可能被恶意构造利用。
Q:什么数据绝不能直接 readObject?
A:来自用户、网络、消息队列、文件上传等不可信来源的数据。
Q:JEP 290 解决什么问题? A:提供对象输入过滤能力,可限制类型、深度、引用数和字节数。
Q:序列化格式选型看什么? A:跨语言、性能、体积、可读性、Schema 演进和安全性。
Q:JSON 的优势是什么? A:可读性和调试体验好,Web 生态成熟。
Q:Protobuf 的优势是什么? A:体积小、性能好、Schema 明确、跨语言能力强。
Q:为什么序列化要考虑版本演进? A:字段新增、删除、重命名都会影响新旧数据兼容。
Q:看到 ObjectInputStream 应该警惕什么?
A:数据来源、类型白名单、依赖 gadget、对象大小和异常处理。
# 8. 总结
Java 原生序列化适合学习对象到字节的基本概念,但不适合作为现代分布式系统的默认协议。资深工程师在看到 ObjectInputStream.readObject() 处理外部输入时,应立即关注安全边界、类型过滤、依赖风险和替代方案。