必须重写 resolveClass 方法实现白名单校验,因类加载发生在该方法内部且早于 readObject,仅靠外部过滤或异常捕获无法阻止静态块执行;需通过 desc.getName() 获取类名并做前缀匹配,非法时抛 ClassNotFoundException。

关键在于重写 resolveClass 方法,在类加载前做白名单校验,而不是依赖反序列化后的逻辑拦截。
为什么必须重写 resolveClass
ObjectInputStream 的类加载发生在 resolveClass 内部,此时类尚未真正加载,也未触发 readObject。若只在 readObject 前检查或用外部过滤器延迟控制,攻击者可能已绕过校验——尤其是 JDK 8 及更早版本不支持 ObjectInputFilter,这是唯一可靠方式。
- 不能靠 try-catch 捕获异常后拒绝:类可能已在 resolveClass 中被加载并执行静态块
- 不能用
Class.forName()校验再放行:这会主动触发类加载,带来风险 - 必须从
ObjectStreamClass desc获取类名(desc.getName()),不解析、不加载
限定包名的白名单实现
通过字符串前缀匹配或正则判断类名是否属于允许的包路径,例如只允许 com.myapp.dto. 及其子包下的类。
- 使用
Set<String>存储完整类名(如"com.myapp.dto.User")适合小范围精确控制 - 对包路径控制,推荐用
desc.getName().startsWith("com.myapp.dto.") - 若需支持嵌套子包(如
com.myapp.dto.order),确保末尾带点号,避免误匹配com.myapp.dtox - 遇到非法类名时,直接抛
ClassNotFoundException;抛InvalidClassException可能被某些框架忽略或降级处理
安全构造与使用要点
自定义子类本身要保证不可被绕过,且每次反序列化都必须实例化该安全子类,不能混用原生 ObjectInputStream。
立即学习“Java免费学习笔记(深入)”;
- 构造函数必须调用
super(in),且不暴露原始输入流操作入口 - 禁止提供 public 的无参构造或可反射实例化的空构造器
- 建议将白名单逻辑封装为 final 静态集合,避免运行时被篡改
- 在业务代码中显式使用:
new SafeObjectInputStream(inputStream).readObject(),而非 new ObjectInputStream - 配合
transient字段和serialVersionUID管理,防止字段篡改导致意外行为
兼容低版本 JDK 的实际写法
以下是一个适用于 JDK 7/8 的最小可行示例:
class SafeObjectInputStream extends ObjectInputStream {
private static final String ALLOWED_PACKAGE = "com.myapp.dto.";
protected SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
if (className != null && className.startsWith(ALLOWED_PACKAGE)) {
return super.resolveClass(desc);
}
throw new ClassNotFoundException("Class not allowed: " + className);
}
}
注意:该类应放在受保护包内,避免被恶意反射调用;生产环境建议结合模块系统(JDK 9+)或 SecurityManager 进一步隔离。


















