登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java ObjectInputFilter 怎样限制反序列化对象

来源:17golang原创

时间:2026-10-18 01:12:03 307浏览 收藏

我第一次把 Java 的对象流过滤加到旧模块时,最容易混淆的是“限制类”与“验证业务数据”这两件事。ObjectInputFilter 能在对象被读入时检查类名、数组长度、引用数、对象图深度和累计字节数,但它不会把不可信的 Java 原生序列化变成通用的数据交换协议。更稳妥的做法是优先换成明确的 JSON、协议缓冲区等格式;确实需要兼容对象流时,再为每个读取入口补上边界。

本文用一个小实验把边界落到代码上:先限制允许出现的类,再给对象图设置资源上限,最后解释 ALLOWED、REJECTED 和 UNDECIDED 应该怎样组合。

先把 ObjectInputFilter 放在正确的位置

ObjectInputStream 负责从字节流恢复对象;过滤器是读取过程中的一道检查边界。它看到的不是业务字段校验结果,而是当前对象图的结构信息,例如正在读取的类、数组长度、引用数量、深度和已经消费的字节数。

如果入口仍然必须读取 Java 原生序列化,先设置流级过滤器,再把允许的类和资源上限写清楚;不要只写一个宽泛的黑名单,也不要把过滤器当成业务数据校验器。
Java 对象流与 ObjectInputFilter 的静态边界结构图,展示类允许集合和对象图资源限制
图1:静态说明图。外层是读取边界,内层分别表达类名规则与对象图资源上限,不代表真实运行截图。

这张图里最值得记住的是两个分组:类名规则决定“可以出现什么类型”,资源限制决定“对象图最多占用多少结构预算”。两者共同作用时,仍然应该让输入来源、协议格式和业务字段校验各自承担清晰职责。

为单个 ObjectInputStream 设置过滤器

先准备一个只在内存中读写的小对象。示例的重点不是对象内容,而是过滤器如何附着到具体的 ObjectInputStream。过滤器必须在第一次读取对象前设置。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

record UserNote(String owner, String text) implements Serializable {}

public class FilterDemo {
    public static void main(String[] args) throws Exception {
        byte[] payload;
        try (var bytes = new ByteArrayOutputStream();
             var output = new ObjectOutputStream(bytes)) {
            // 只构造演示输入;生产系统不要把不可信字节流直接当作可信对象。
            output.writeObject(new UserNote("demo", "hello"));
            output.flush();
            payload = bytes.toByteArray();
        }

        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
                "FilterDemo$UserNote;java.base/*;maxdepth=8;maxrefs=32;maxbytes=4096;!*"
        );

        try (var input = new ObjectInputStream(new ByteArrayInputStream(payload))) {
            // 必须在 readObject 前设置,避免先读入对象再补限制。
            input.setObjectInputFilter(filter);
            Object value = input.readObject();
            System.out.println(value.getClass().getName());
        }
    }
}

模式字符串按顺序表达允许项和资源上限,最后的 !* 让没有明确匹配的类进入拒绝分支。这里的类名要按实际编译后的名称填写;如果把嵌套类、代理类或集合类型加入允许集合,应该逐项说明原因,不要为了“先跑起来”直接使用无限制的通配符。

对我来说,最实用的检查点是:过滤器附着在每个读取入口,而不是只在某个工具类里创建一次;setObjectInputFilter 发生在 readObject() 之前;资源上限也根据真实对象图调整,而不是照抄一个看起来很大的数字。

用程序化过滤器表达更细的边界

模式字符串适合稳定的类名集合和通用上限。遇到数组大小、引用数或某类需要特殊判断的场景,可以直接实现 ObjectInputFilter。过滤器返回的是状态,不是业务异常:拒绝时由对象流终止读取,继续判断则保留组合空间。

ObjectInputFilter limits = info -> {
    // 类名可能为空;资源指标则用来限制对象图的整体规模。
    Class> serialClass = info.serialClass();
    if (serialClass != null && serialClass != UserNote.class
            && serialClass != String.class) {
        return ObjectInputFilter.Status.REJECTED;
    }
    if (info.depth() > 8 || info.references() > 32
            || info.streamBytes() > 4096) {
        return ObjectInputFilter.Status.REJECTED;
    }
    // 没有触发本地规则时交给组合中的其它过滤器继续判断。
    return ObjectInputFilter.Status.UNDECIDED;
};

try (var input = new ObjectInputStream(new ByteArrayInputStream(payload))) {
    // 程序化过滤器同样必须在第一次读取前设置。
    input.setObjectInputFilter(limits);
    Object value = input.readObject();
}
Java ObjectInputFilter 的状态与对象流静态关系图,展示 FilterInfo、资源指标和三种 Status
图2:静态结构图。它把 FilterInfo、资源指标与三种 Status 放在同一关系面板中,帮助理解代码返回值,不是运行结果截图。

代码中没有把所有情况都返回 ALLOWED,是因为“我没有发现问题”不等于“输入一定应该放行”。REJECTED 表示明确拒绝,ALLOWED 表示当前规则明确允许,UNDECIDED 表示把决定留给组合中的另一层。若团队要采用组合过滤器,应该把每层的责任写在配置说明里。

读懂三种 Status,避免把过滤器写反

Status适合表达的意思使用时要注意
ALLOWED当前规则明确接受这个类或资源状态不要因为类名匹配就忽略深度、引用数和字节预算
REJECTED当前规则明确不接受,读取应停止拒绝原因应能从日志或配置映射回具体边界
UNDECIDED本层没有结论,交给其它过滤器继续判断不能把它误读成“已经安全放行”

如果过滤规则既要允许业务类,又要保留资源上限,常见做法是先用类名模式收窄范围,再用程序化过滤器补充上限和特殊类型判断。每增加一层,都要确认组合后的最终状态,而不是只看其中一段配置。

什么时候放到 JVM 级配置

Oracle 文档还提供了 jdk.serialFilter 和过滤器工厂等 JVM 级入口。它们适合为一组应用统一提供最低限度的保护边界,但不应替代每个业务读取点的明确协议设计。JVM 级配置通常由启动参数或安全属性注入,使用前要把应用中实际需要的类集合和对象规模记录下来。

官方 API 说明:

  • https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/io/ObjectInputFilter.html
  • https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/io/ObjectInputStream.html

如果过滤器已经拒绝了输入,排查顺序可以固定成三步:先确认是类名规则还是资源上限触发;再对照对象图的实际类型和大小;最后决定是修正格式、调整边界,还是让该入口停止读取这类数据。不要通过不断放宽 !* 或取消上限来掩盖协议不清的问题。

我会保留的最小检查清单

  1. 不可信输入是否仍在使用 Java 原生序列化;能否迁移到明确的数据格式。
  2. 每个 ObjectInputStream 是否在第一次读取前设置了过滤器。
  3. 允许类是否是最小集合,是否误用了无限制通配符。
  4. 深度、引用数、数组长度和字节数是否都有符合业务的上限。
  5. 团队是否记录了 ALLOWED、REJECTED、UNDECIDED 各自的组合责任。

这套写法适合需要暂时兼容对象流的旧系统:边界清楚、改动集中,也方便后续迁移。新系统如果可以选择协议,我仍然会优先选结构明确、字段可审计的格式,再把 ObjectInputFilter 当作兼容期的防线,而不是长期的数据模型。

相关问题

ObjectInputFilter 能代替 JSON 字段校验吗?

不能。它主要检查类和对象图资源指标;业务字段的取值、必填关系和版本兼容仍然需要由数据格式与业务代码负责。

为什么只返回 UNDECIDED 不等于允许?

因为它代表当前过滤器没有结论,组合中的其它过滤器仍可能拒绝;只有明确返回 ALLOWED 才表达本层的放行意图。

流级过滤器和 JVM 级过滤器怎么选?

流级过滤器更贴近单个入口和业务对象;JVM 级配置适合作为统一底线。实际项目通常需要两者配合,并把迁移到明确协议作为长期方向。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>