• Effective java 总结11 - 序列化


    Effective java 总结11 - 序列化

    序列化:对象 -> 字节流

    反序列化:字节流 -> 对象

    第85条 其他方法优先于java序列化

    序列化的根本问题在于:攻击面过于庞大,无法防护,将不信任的流进行反序列化,可能导致RCE,DOS攻击

    RCE: remote code execution 远程代码执行

    DOS:denial of service 拒绝服务

    避免序列化攻击的最佳方法是永远不要反序列化任何东西

    在编写新系统中没有理由再使用java序列化

    永远不要反序列化不被信任的数据,如果必要,使用对象的反序列化过滤

    替换为: 跨平台结构化数据表示法

    JSON:java, 基于文本,可以阅读,非常高效

    Protocol Buffers:C++,基于二进制


    第86条 谨慎实现Serializable接口

    使一个类的实例可以被序列化,只需要在类的声明中加入 implements Serializable 即可

    实现Serializable接口付出的代价

    • 一个类一旦被发布,大大降低了这个类被改变的灵活性,因为如果接受了默认的序列化形式,这个类中私有的和包级私有的实例域都将变成导出API的一部分

      • 序列化使类的演变受到限制,限制与流的唯一标识符有关,通常为静态私有final的序列版本UID(SerialVersionUID),如果没有显示指定,则系统会对类的结构运用一个加密散列函数自动产生标识号,这个自动产生的号码会受类名称、接口名称、所有公有、受保护的成员的名称影响
    • 增加了出现BUG和安全漏洞的可能性, 反序列化机制都是一个“隐藏的构造器”

    • 随着类发行新的版本,相关的测试负担增加

    实现Serializable接口并不是一个很轻松就可以做出的决定

    • 应该实现Serializable的类:BigInteger、 Instant等值类

    • 不应该实现Serializable的类: 代表活动实体的类(线程池 thread pool)等

    为了继承而设计的类应该尽可能少去实现Serializable接口,例外:

    • Throwable, RMI异常可以从服务端传到客户端
    • Component, GUI可以被发送,保存,复制

    其他

    • 内部类不应该实现Serializable接口
    • 静态成员类可以实现Serializable接口

    第87条 考虑使用自定义的序列化形式

    没有事先认真考虑默认的序列化形式是否合适,则不要贸然接受

    如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式

    public class Name implements Serializable{
        private final String lastName;
        private final String firstName;
        private final String middleName;
    }
    // 通常必须提供一个readObject方法以保证约束关系和安全性(保证lastName、等非NULL)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果一个对象的物理表示法与它的逻辑内容有实质区别时,使用默认的序列化有很多缺点

    public final class StringList implements Serializable{
        private int size = 0; 
        private Entry head = null;
        
        private static class Entry implements Serializable{
            String Data;
            Entry next;
            Entry previous;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 使这个类的导出API永远束缚在该类的内部表示法上
    • 会消耗过多的空间、时间
    • 有可能引起栈溢出
    • 对于散列表而言,接受默认的序列化形式将会构成严重的BUG

    修订优化版本

    public final class StringList implements Serializable{
        private transient int size = 0; 
        private transient Entry head = null;
        
        private static class Entry {
            String Data;
            Entry next;
            Entry previous;
        }
        ...
        private void writeObject(ObjectOutputStream s) throws IOException{
            s.defaultWriteObject();
            s.writeInt(size);
            
            for(Entry e = head; e != null, e = e.next){
                s.writeObject(e.data);
            }
        }
        private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
            s.defaultReadObject();
            int numElements = s.readInt();
            
            for(int i = 0; i < numElements; i++){
                add( (String)s.readObject() );
            }
        }
    }
    // transient 修饰符表明这个实例域将从一个默认序列化形式中省略掉
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • writeObject : 序列化; readObject:反序列化

    • 调用writeObject 、readObject得到的序列化形式允许在以后的发行版本中增加非瞬时的实例域,且保持向前、向后的兼容性

    • defaultWriteObject()被调用的时候,每一个未被transient标记的实例域都会被序列化

    • 自定义的序列化形式中,大多数的or全部实例域都应被transient标记

    • 默认的序列化形式中,多个域被transient标记,则将被初始化为默认值,若不能接受默认值,则必须提供readObject方法,调用defaultReadObject,将这些域恢复为可用接受的值,或者延迟初始化

    无论哪种形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步

    private synchronized void writeObject(ObjectOutputStream s) throws IOException{
        s.defaultWriteObject();
    }
    
    • 1
    • 2
    • 3

    无论哪种形式,都要为自己编写的每个可序列化的类声明一个显示的序列版本UID

    private static final long serialVersionUID = randomLongValue;
    // serialVersionUID 的值是什么并不重要,主要标记当前类的版本
    // 不要修改序列版本UID,否则将会破坏类现有的已被序列化实例的兼容性
    
    • 1
    • 2
    • 3

    第88条 保护性地编写readObject方法

    public final class Period{
        private final Date start;
        private final Date end;
        
        public Period(Date start, Date end){
            if(start.compareTo(end) > 0) throw new...;  // 约束 结束时间 > 开始时间
            this.start = start;
            this.end = end;
        }
        
        public Date start(){
            return new Date(start.getTime()); //保护性clone
        }
        
        public Date end(){
            return end;
            return new Date(end.getTime()); //保护性clone
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    增加 implements Serializable 做成可序列化,存在的一些问题:

    readObject 方法实际上相当于公有的构造器,必须检查其参数的有效性,且必要的时候对参数进行保护性拷贝

    // 序列化一个违反Period约束的字节流
    public class BogusPeriod{
        private static final byte[] serializedForm = {0x50, 0x65, ...};
        
        public static void main(String[] args){
            Period p = (Period) deserialize(serializedForm);
            sout(p);
        }
        
        static Object deserialize(byte[] bf){
            try{
                return new ObjectInputStream(new ByteArrayInputStream(bf)).readObject();
            }catch(...){...}
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    // 伪造字节流,创建可变的Period实例: 以一个有效的Period实例开头,附带两个额外引用,指向Period中两个私有的Date域,攻击这两个“恶意编制”的对象引用
    public class MutablePeriod{
        public final Period period;
        public final Date start;
        public final Date end; 
        
        public MutablePeriod(){
            try{
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(bos);
                
                out.writeObject(new Period(new Date(), new Date()));
                byte[] ref = {0x71, 0, 0x7e, 0, 5};
                bos.write(ref);
                ref[4] = 4;
                bos.write(ref);
                
                ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
                period = (Period)in.readObject();
                start = (Period)in.readObject();
                end = (Period)in.readObject();
            }catch(){}
        }
    }
    // attack
    public static void main(String[] args){
        MutablePeriod mp = new MutablePeriod();
        Period p = mp.period;
        Date pEnd = mp.end;
        pEnd.setYear(78);
        sout(p);
        pEnd.setYear(49);
        sout(p);
    }
    // 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    检查其参数的有效性,且对参数进行保护性拷贝

    private readObject(ObjectInputStream s){
        s.defaultReadObjec();
        // 保护性拷贝
        start = new Date(start.getTime());  // 需要去掉Period中 start,end域的final
        end = new Date(end.getTime());
        // 有效性检查
        if(start.compareTo(end) > 0) {
            throw ...;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    编写健壮的readObject()方法:

    • 对于对象引用必须保持为私有的类,要保护性拷贝这些域中的每个对象
    • 对于有约束的对象,进行约束检查
    • 无论是直接还是间接方式,不要调用类中任何可被覆盖的方法

    第89条 对于实例控制,枚举类型优于readResolve

    // 单例
    public class Elvis{
        public static final Elvis INSTANCE = new Elvis();
        private Elvis(){}
        private String[] fs = {"aaa", "bbb"};
        // private transient String[] fs = {"aaa", "bbb"};  // 应该加上 transient
        ......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    任何一个readObject方法,不管显式还是默认的,都会返回一个新建的实例,这个实例不同于类初始化时创建的实例,包括单例

    readResolve特性允许用readObject创建的实例代替另一个实例

    // Elvis类中添加
    private Object readResolve{
        return INSTANCE;
    }
    // 忽略被反序列化的对象,只返回该类初始化时创建的特殊的实例
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果依赖readResolve进行实例控制,带有对象引用的所有实例域都应该被声明成 transient, 或者所有的实例域都为基本类型,否则会被攻击盗用(内部域的盗用攻击)

    如果将一个可序列化的实例受控的类编写成枚举,java可以绝对保证除了声明的常量外,没有其他实例

    // ENUM
    public enum Elvis{
        INSTANCE;
        private String[] fs = {"aaa", "bbb"};
        public void printfs(){
            sout(Arrays.toString(fs));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    readResolve的可访问性

    readResolve私有 – final类

    readResolve包级私有 – 同一个包中的子类

    readResolve受保护的or公共的 – 所有没有覆盖它的子类


    第90条 考虑用序列化代理代替序列化实例

    序列化代理模式:

    1、为可序列化的类设计一个私有的静态嵌套类 – 序列化代理

    2、嵌套类有一个单独的构造器,参数类型为外围类 – 只从参数中复制数据

    3、无需进行一致性检查和保护性拷贝

    4、外围类和序列化代理类必须声明实现Serializable接口

    public final class Period{
        private final Date start;
        private final Date end;
        
        public Period(Date start, Date end){
            if(start.compareTo(end) > 0) throw new...;  // 约束 结束时间 > 开始时间
            this.start = start;
            this.end = end;
        }
        
        public Date start(){
            return new Date(start.getTime()); //保护性clone
        }
        
        public Date end(){
            return end;
            return new Date(end.getTime()); //保护性clone
        }
        // 私有静态嵌套类
        private static class SerializationProxy implements Serializable{
        	private final Date start;
        	private final Date end;
        
        	SerializationProxy(Period p){
            	start = p.start;
            	end = p.end; 
            }
        	private static final long serialVersionUID = 12313413412341;
            
            // 返回逻辑上相当的外围类实例, 在反序列化时将序列化的代理转变回外围类实例
        	private Object readResolve(){
            	return new Period(start, end); // use public constructor
        	}
    	}
        // 将外围类的实例转换为序列化代理
        private Object writeReplace(){
            return new SerializationProxy(this);
        }
        
        // 防御伪造序列化攻击
        private void readObject(ObjectInputStream os) throws InvalidObjectException{
            throw new InvalidOjectException("Proxy required");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    writeReplace: 通过序列化代理,这个方法可以被逐字复制到任何类中

    readObject:防御伪造序列化攻击(攻击者有可能伪造,企图违反该类的约束条件)

    readResolve:不必单独确保被反序列化的实例一定要遵守类的约束条件

    序列化代理模式优点

    • 可以阻止伪字节流的攻击以及内部域的调用攻击

    • 允许period类的域为final(确保Period类真正不可变的关键)

    • 不必显示执行有效性检查

    • 允许反序列化实例有着与原实例不同的类

      // EnumSet静态工厂返回两种EnumSet实例:枚举类型<=64:RegularEnumSet,  枚举类型>64:JumboEmumSet
      private static class SerializationProxy <E extends Enum<E>> implements Serializable{
          private final Class<E> elementType;
          private final Enum<?>[] elements;
          
          SerializationProxy(EnumSet<E> set){
              elementType = set.elementType;
              elements = set.toArray(new Enum<?>[0]);
          }
          
          private Object readResolve(){
              EnumSet<E> result = EnumSet.noneof(elementType);
              for(Enum<?> e: elements){
                  result.add((E)e);
              }
              return result;
          }
          private static final long serialVersionUID = 43123813913889890L;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

    序列化代理模式的局限

    • 不能与可以被客户端扩展的类相兼容
    • 不能与对象图中包含循环的某些类相兼容?
    • 开销增加

    当发现需要在一个不能被客户端扩展的类上写readObject或者writeObject方法时,应该考虑使用序列化代理模式

  • 相关阅读:
    如何定义set的比较函数?
    CSS3-2D缩放
    从CNN到Transformer:基于PyTorch的遥感影像、无人机影像的地物分类、目标检测、语义分割和点云分类
    JavaScript之BOM与DOM操作
    Excel查找函数的高级用法
    阿里资深专家撰写出的Nginx底层与源码分析手册,GitHub已爆赞
    电子表电路
    编程向导-JavaScript-基础语法-算术/赋值/逗号/运算符
    刷题记录(NC20313 [SDOI2008]仪仗队)
    LINUX笔记温习
  • 原文地址:https://blog.csdn.net/qq_34595352/article/details/125885157