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)

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

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;
    }
}
  • 使这个类的导出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 修饰符表明这个实例域将从一个默认序列化形式中省略掉
  • writeObject : 序列化; readObject:反序列化

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

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

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

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

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

private synchronized void writeObject(ObjectOutputStream s) throws IOException{
    s.defaultWriteObject();
}

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

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

第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
    }
}

增加 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(...){...}
    }
}
// 伪造字节流,创建可变的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);
}
// 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝

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

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 ...;
    }
}

编写健壮的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
    ......
}

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

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

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

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

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

// ENUM
public enum Elvis{
    INSTANCE;
    private String[] fs = {"aaa", "bbb"};
    public void printfs(){
        sout(Arrays.toString(fs));
    }
}

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

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

序列化代理模式的局限

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值