什么是序列化
把Java对象变成一串字节(byte流),这样就可以存到文件里或者通过网络传出去
比喻:你要搬家,把家具拆成零件打包装箱(序列化),到了新家再组装回来(反序列化)
序列化:对象 → 字节流(ObjectOutputStream)
反序列化:字节流 → 对象(ObjectInputStream)
常见使用场景
把对象存到文件或数据库(持久化)
通过网络传输对象(RPC调用、分布式系统)
深拷贝对象(序列化再反序列化,得到一个全新的副本)
Serializable接口
Java中最简单的接口之一——标记接口,里面一个方法都没有
它的作用就是告诉JVM:”这个类的对象允许被序列化”
1 | // 点进去看源码,就这么点东西 |
如果不实现Serializable就去序列化会怎样?
直接抛 NotSerializableException,程序报错
serialVersionUID
序列化的”版本号”,用来确保序列化和反序列化时是同一个版本的类
比喻:就像软件的版本号,你用v1.0打包的数据,v2.0的程序可能读不了
1 | public class User implements Serializable { |
不写serialVersionUID会怎样?
JVM会根据类的结构自动生成一个
但是!你改了类的任何字段(加个字段、删个字段),自动生成的UID就变了
之前序列化保存的数据就反序列化不回来了,报 InvalidClassException
所以实战建议:只要实现了Serializable,就手动写上serialVersionUID
IDE(IDEA/Eclipse)都有快捷方式自动生成,一般写 1L 就行
transient关键字
标记某个字段不参与序列化
应用场景:密码、临时计算结果、缓存数据等不需要/不应该持久化的字段
1 | public class User implements Serializable { |
| 关键字 | 效果 |
|---|---|
| 普通字段 | 正常序列化和反序列化 |
transient 字段 |
序列化时跳过,反序列化后是默认值(int=0, String=null, boolean=false) |
static 字段 |
不参与序列化(static属于类,不属于对象) |
代码示例:序列化和反序列化
1 |
|
反序列化安全问题
这是安全领域的重灾区,Java反序列化漏洞多年来一直是Web安全的热点
为什么反序列化危险?
反序列化时,JVM会自动调用类的 readObject() 方法来重建对象
如果攻击者能控制输入的字节流,就能让JVM去执行任意代码
关键点:你以为你在读一个普通对象,其实攻击者塞进来的是一个”炸弹”
Apache Commons Collections 反序列化链(经典利用链)
这是最早被公开的Java反序列化利用链,原理简述:
1 | 攻击者构造恶意序列化数据 |
核心思路:找到一条从 readObject() 到 Runtime.exec() 的调用链
工具:ysoserial(专门生成各种反序列化利用链的payload)
真实案例
| 漏洞 | 影响范围 | 简述 |
|---|---|---|
| WebLogic反序列化 (CVE-2015-4852等) | Oracle WebLogic Server | T3协议接收序列化数据,未做过滤,直接反序列化导致RCE |
| Fastjson反序列化 (多个CVE) | 阿里巴巴Fastjson库 | JSON反序列化时支持 @type 指定类名,攻击者指定恶意类触发RCE |
| Apache Shiro (CVE-2016-4437) | Apache Shiro认证框架 | RememberMe功能用了AES加密+Java序列化,密钥硬编码被破解后可构造恶意数据 |
| Jenkins (CVE-2017-1000353) | Jenkins CI/CD | CLI通道接收序列化对象,未认证即可触发反序列化RCE |
防御措施
1. 不要反序列化不可信数据(最根本的防御)
如果数据来自用户、网络、外部系统,别用Java原生反序列化
2. 用JSON/XML代替Java原生序列化
Jackson、Gson这些JSON库比Java原生序列化安全得多
但注意:Fastjson的autoType功能也出过问题,需要关闭或设白名单
3. 白名单校验
自定义ObjectInputStream,重写 resolveClass() 方法,只允许特定类被反序列化
1 | public class SafeObjectInputStream extends ObjectInputStream { |
4. Java 9+ 反序列化过滤器(ObjectInputFilter)
1 | ObjectInputStream ois = new ObjectInputStream(inputStream); |
5. 及时更新依赖库,移除不必要的Gadget库(如老版本的commons-collections)
序列化方式对比
| 对比项 | Java原生序列化 | JSON(Jackson/Gson) | Protobuf |
|---|---|---|---|
| 可读性 | 二进制,不可读 | 文本,人类可读 | 二进制,不可读 |
| 性能 | 一般 | 一般 | 很快 |
| 体积 | 较大 | 中等 | 很小 |
| 跨语言 | 仅Java | 几乎所有语言 | 几乎所有语言 |
| 安全性 | 差(反序列化漏洞多) | 较好(注意autoType) | 好 |
| 使用场景 | 几乎不推荐使用 | Web API、配置文件 | 微服务通信、高性能场景 |
结论:实际项目中,优先用JSON或Protobuf,避免Java原生序列化
常见坑
坑1:类实现了Serializable但内部字段的类没有实现
1 | public class Order implements Serializable { |
规则:对象图中所有被引用的对象都必须是可序列化的
坑2:修改了类的字段结构但没改serialVersionUID
如果你加了字段、删了字段,但serialVersionUID没变
反序列化时:新加的字段会是默认值,删掉的字段被忽略——不会报错但数据可能不对
如果serialVersionUID变了——直接 InvalidClassException
坑3:单例类被反序列化破坏
反序列化会创建新对象,绕过构造方法,所以单例模式会被破坏
解决办法:在类中添加 readResolve() 方法
1 | public class Singleton implements Serializable { |
坑4:用Serializable传输敏感数据没加transient
密码、密钥、token等敏感信息,序列化后就是明文字节流
一定要用 transient 标记,或者干脆用JSON+字段过滤
练习题
题1:以下代码序列化后反序列化,各字段的值是什么?
1 | public class Student implements Serializable { |
题2:serialVersionUID的作用是什么?不写会有什么问题?
作用:序列化版本控制,确保序列化和反序列化的类版本一致
不写:JVM自动生成,一旦类结构改变,UID跟着变,之前序列化的数据就读不回来了
题3:为什么说Java原生反序列化不安全?如何防御?
因为反序列化会自动调用 readObject(),攻击者可以构造恶意序列化数据触发任意代码执行
防御:白名单校验、用JSON代替原生序列化、使用ObjectInputFilter、不反序列化不可信数据
相关笔记
异常体系 | 自定义异常 | try-catch-finally
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 自定义异常 | java基础 | IO流 |