如何正确使用Java序列化?

前言

  什么是序列化:将对象编码成一个字节流,这样一来就能够在通讯中传递对象了。好比在一台虚拟机中被传递到另外一台虚拟机中,或者字节流存储到磁盘上。java

  “关于Java的序列化,无非就是简单的实现Serializable接口”这样的说法只能说明停留在会用的阶段,而咱们想要走的更远每每就须要了解更多的东西,好比:为何要实现序列化?序列化对程序的安全性有啥影响?如何避免多余的序列化?.....安全

  本文主要参考资料《Effective Java》,其中代码除了只做部分说明,不能运行外,剩余代码都是亲自实践过的!ide

 


 

 

1、序列化代价

虽然实现Serializable很简单,可是为了序列化而付出的长期开销每每是实实在在的。实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大下降了“改变这个类的实现”的灵活性。工具

  问:这个灵活性具体是指什么呢?测试

  即一旦类实现了Serializable接口,而且这个类被普遍地使用,每每必须永远支持这种序列化形式,若是使用默认的序列化形式,那么这种序列化形式将永远地束缚在该类最初的内部表示法上,换句话说,一旦接受了默认的序列化形式,这个类中私有的和包级私有的实例域都变成导出的API的一部分,这显然是不符合的。这也就是实现序列化每每须要考虑到的几个代价,具体请往下看!this

一、可能会致使InvalidClassException异常

  若是没有显式声明序列版本UID,对对象的需求进行了改动,那么兼容性将会遭到破坏,在运行时致使InvalidClassException。好比:增长一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化,则会出现序列版本UID不一致的状况。因此最好仍是显式的增长序列版本号UID。编码

  对User JavaBean实现Serializable接口,增长固定的序列版本号spa

public class User implements Serializable {

    /** 显示增长序列版本UUID,自动生成UUID可能会致使InvalidClassException */
    private static final long serialVersionUID = 1L;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

  使用ObjectOutputStream与ObjectInputStream流控制序列与反序列3d

/**
 * @author jian
 * @date 2019/4/5
 * @description 测试序列化
 */
public class SeriablizableTest {

    public static void main(String[] args) {
        User user = new User(1, "lijian");
        serializeUser(user);
        deserializeUser();

    }

    /**
     * 使用writeObject方法序列化
     *
     * @param user
     */
    private static void serializeUser(User user) {
        ObjectOutputStream outputStream = null;
        try {
            // 建立对象输出流, 包装一个其它类型目标输出流,如文件流
            outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
            // 经过对象输出流的writeObject方法将对象user写入流中
            outputStream.writeObject(user);
            System.out.println("user序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void deserializeUser() {
        User user = null;
        Employee employee = null;
        ObjectInputStream inputStream = null;
        try {
            // 建立对象输出流, 包装一个其它类型目标输出流,如文件流
            inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
            // 经过对象输出流的writeObject方法将对象user写入流中
            user = (User)inputStream.readObject();
            System.out.println("user反序列化成功:" + user);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }  catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

输出结果:先看user.txt文件中二进制文件流(由于txt打不开二进制流,因此是乱码)code

以后再看控制台中,反序列化输出的User{id=1, name='lijian'},说明整个过程序列化成功!

以后去掉固定的序列版本号UID,让其自动生成,同时增长age属性(或者手动修改UID为2L)

 private static final long serialVersionUID = 2L;

只进行反序列化将会报错: java.io.InvalidClassException 

 public static void main(String[] args) {
        User user = new User(1, "lijian");
//        serializeUser(user);
        deserializeUser();

    }

 

 

二、增长了出现Bug和安全漏洞的可能性

  序列化机制是一种语言以外的对象建立机制,反序列化机制都是一个“隐藏的构造器”,具有与其余构造器相同的特色,正式由于反序列化中没有显式构造器,因此很容易就会忽略:不容许攻击者访问正在构造过程当中的对象内部信息。换句话说,序列化后的字节流能够被截取进行伪造,以后利用readObject方法反序列会不符合要求甚至不安全的实例。

    

 

 

三、随着类发行新的版本,测试负担也会增长。

  一个可序列化的类被修订时,须要检查是否“在新版本中序列化一个实例,能够在旧版本中反序列化”,若是一个实现序列化的类有不少的子类或者是被修改时,就不得不加以测试。

 

2、序列化的缺陷

一、序列化是保存对象的状态,也就是不会关心static静态域,静态域不会被序列化。如User中count静态域。

public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private static int count = 1;

    public User(int id, String name) {
        // 约束条件name不能为null
        if (name == null || StringUtils.isEmpty(name)) {
            throw new NullPointerException("name is null");
        }
        this.id = id;
        this.name = name;
    }
    public User(){};

    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        User.count = count;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", count=" + count +
                '}';
    }

    private void readObject(ObjectInputStream inputStream)
            throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();
        // 约束条件name不能为null
        if (name == null || StringUtils.isEmpty(name)) {
            throw new NullPointerException("name is null");
        }
    }

}

赋值count为20:

public static void main(String[] args) {
        User user = new User();
        user.setName("Lijian");
        user.setId(1);
        user.setCount(20);
        serializeUser(user);
        deserializeUser();
}

序列化-反序列化

/**
     * 使用writeObject方法序列化
     *
     * @param user
     */
    private static void serializeUser(User user) {
        ObjectOutputStream outputStream = null;
        try {
            // 建立对象输出流, 包装一个其它类型目标输出流,如文件流
            outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
            // 经过对象输出流的writeObject方法将对象user写入流中
            outputStream.writeObject(user);
            System.out.println("user序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void deserializeUser() {
        User user = null;
        ObjectInputStream inputStream = null;
        try {
            // 建立对象输出流, 包装一个其它类型目标输出流,如文件流
            inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
            // 经过对象输出流的writeObject方法将对象user写入流中
            user = (User)inputStream.readObject();
            // User静态变量初始化为0,不会被反序列化
            System.out.println("user反序列化成功!");
            System.out.println("id:" + user.getId());
            System.out.println("name:" + user.getName());
            System.out.println("count:" + user.getCount());
        }  catch (ClassNotFoundException e) {
            e.printStackTrace();
        }  catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

控制它输出:count明明被赋值为20,可是反序列化后输出为0,说明static是不会参数序列化的,跟transient相似。最终在反序列化过程当中会被初始化为默认值(基本数据类型为0,对象引用为null,boolean为false)

 

二、在序列化对象时,若是该对象中有引用对象域名,那么也要要求该引用对象是可实例化的。如序列化User实例,其中引用了Employee实例,那么也须要对Employee进行可序列化操做,不然会报错: java.io.NotSerializableException 

User增长对Employee引用:

 /** 对外引用其它对象,若是序列化该实例,则该对象实例也必须能实例化(implement Serializable) */
    public Employee employee = new Employee(1, "Java programmer");

 Employee不实现序列化:

public class Employee{
private int code;
    private String position;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

    public Employee(int code, String position) {
        this.code = code;
        this.position = position;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "code=" + code +
                ", position='" + position + '\'' +
                '}';
    }
}

测试类:

/**
 * @author jian
 * @date 2019/4/5
 * @description 测试序列化
 */
public class SeriablizableTest {

    public static void main(String[] args) {
        User user = new User(1, "lijian");
        serializeUser(user);
        deserializeUser();

    }

    /**
     * 使用writeObject方法序列化
     *
     * @param user
     */
    private static void serializeUser(User user) {
        ObjectOutputStream outputStream = null;
        try {
            // 建立对象输出流, 包装一个其它类型目标输出流,如文件流
            outputStream = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
            // 经过对象输出流的writeObject方法将对象user写入流中
            outputStream.writeObject(user);
            System.out.println("user序列化成功!");
        } catch (NotSerializableException e) {
            System.out.println("user引用employee对象域序列化失败");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void deserializeUser() {
        User user = null;
        Employee employee = null;
        int id = 0;
        ObjectInputStream inputStream = null;
        try {
            // 建立对象输出流, 包装一个其它类型目标输出流,如文件流
            inputStream = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
            // 经过对象输出流的writeObject方法将对象user写入流中
            user = (User)inputStream.readObject();
            System.out.println("user引用employee对象域反序列化成功");
            System.out.println("user反序列化成功:" + user);
        } catch (WriteAbortedException e) {
            System.out.println("user引用employee对象域反序列化失败");
        }  catch (ClassNotFoundException e) {
            e.printStackTrace();
        }  catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

 

控制台输出结果:

要解决这样的问题,要么将 Employee implement Serializable ,要么对Employee对象实例transient修饰: public transient Employee employee = new Employee(1, "Java programmer"); 。可是须要注意的是序列化过程会对transient修饰的域初始化为默认值(对象引用为null,基本数据类型为0,boolean为false),因此执行以上代码会出现 java.lang.NullPointerException 

 

三、默认序列化的过程可能消耗大量内存空间和时间,甚至可能会引发栈溢出:由于第二条的缘由,若是一个类中大量存在引用对象域,而且都须要实现序列化,那么整个序列化过程可能会很消耗时间,在通讯传输过程当中更是如此,同时序列化后的字节流须要足够大的内存。

3、提升序列化的安全性

一、编写readObject提供安全性与约束性

  即便肯定了默认的序列化形式是合适的,一般还必须提供一个readObject方法以保证约束关系和安全性。readObject方法至关于另外一个共有构造器(能够认为是用“字节流做为惟一参数”的构造器)跟其它构造器同样,它也要求一样的全部主要事项:构造器必须检查参数的有效性,必要时对参数进行保护性拷贝等。readObject若是没有作到,那么对于攻击者来讲违反这个类的约束条件相对就比较简单了,若是对一我的工仿造的字节流(人工修改从实例序列后的字节流)时,readObject产生的对象会违反所属类的约束条件。

  1)为了解决这个问题,User中须要提供了readObject方法,该方法首先调用defalutReadObject,而后检查被反序列化以后的对象的有效性,若是有效性检查失败,readObject方法就会抛出InvalidObjectException异常,使反序列过程不能成功。

private void readObject(ObjectInputStream inputStream)
            throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();
}

  User中的构造器中已对参数name约束为不能为null

public User(int id, String name) {
        // 约束条件name不能为null或空
        if (name == null || StringUtils.isEmpty(name)) {
            throw new NullPointerException("name is null or empty");
        }
        this.id = id;
        this.name = name;
}

  2)那么readObject中也应该对其name进行约束,不然人工伪造的字节流很容易经过readObject构造出没有任何约束的对象实例,形成安全隐患。

private void readObject(ObjectInputStream inputStream)
            throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();

        // 约束条件name不能为null或空
        if (name == null || StringUtils.isEmpty(name)) {
            throw new NullPointerException("name is null or empty"); 
     }
}

  尽管以上两种修正已经有效地避免攻击者建立无效的User实例,可是还有一种状况经过伪造字节流能够建立可变的User实例:好比User中增长Date对象引用birthday私有域,而后经过附加伪造字节流指向该birthday引用,攻击者从ObjectInputStream中读取User实例,而后读取附加后面的恶意Date引用,经过该Date引用就能够可以访问User对象内部私有Date域所引用的对象,从而改变User实例。

 

代码以下:

public class MutableUser {

    public User user;
    public Date birthday;

    public MutableUser(){
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            // 字节流有效的User实例开头,而后附加额外的引用
            out.writeObject(new User(new Date()));
            // 假设这是恶意的二进制,即附加恶意对象引用Date
            byte[] ref = {0x71, 0, 0x7e, 0 ,5};
            bos.write(ref);
            // 攻击者从ObjectInputStream中读取User实例,而后读取附加在后面的“恶意编制对象引用Date”
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            user = (User) in.readObject();
            birthday = (Date) in.readObject();

        } catch (Exception e) {
      
        }
    }

    public static void main(String[] args) {
        MutableUser mutableUser = new MutableUser();
        User user = mutableUser.user;
        Date birthday = mutableUser.birthday;
        // 攻击者修改User内部birthday私有域,年份更改成2018
        birthday.setTime(2018);
        System.out.println(user);
    }
}

注:以上代码运行不了,只会加以解释说明而已,具体能够查看《Effective Java》中的代码举例

为了解决此问题,提出第三个安全措施

 3)当一个对象被反序列化时,客户端不该该拥有对象的引用,若是哪一个域包含了这样的对象引用,若是包含了私有的域(组件),就必需要保护性拷贝(非final域):当User对象在客户端MutableUser反序列化时,客户端拥有 了不应拥有的User私有域Date引用birthday,因此应该在readObject对birthday进行拷贝:

 private void readObject(ObjectInputStream inputStream)
            throws IOException, ClassNotFoundException {
        inputStream.defaultReadObject();
        // 保护性拷贝birthday
        birthday = new Date(birthday.getTime());
        // 约束条件name不能为null
        if (name == null || StringUtils.isEmpty(name)) {
            throw new NullPointerException("name is null");
        }
    }

 

总结:

  1)使用readObject其实就跟正常无参数的构造器同样,该知足的约束须要知足,同时必要时进行保护性拷贝。

  2)反序列化过程最终会调用readObject方法,以下是一个异常栈的调用关系(代码中故意让readObject方法抛异常):deserialize---->ObjectInputStream.readObject----->ObjectInputStream.readObject0----->......User.readObject

  

 

二、使用readResolve加强单例

  可是若是Sinleton类实现了序列化,那么它再也不是一个Singleton,不管该类使用了默认的序列化形式,仍是自定义的序列化形式,仍是是否提供显式的readObject方法都不要紧。任何一个readObject方法,不论是显式仍是默认的,它都会返回一个新建的实例,这个新建的实例不一样于该类初始化时建立的实例。

  简单的Singleton:

public class Singleton {

    private static Singleton INSTANCE= new Singleton();
    private Singleton(){};
  .....
}

  readResolve特性容许使用readObject建立实例代替另外一个实例,若是一个类定义了readResolve方法,而且具有正确的声明,那么在反序列化的以后,新建的readResolve方法就会被调用,而后返回的对象引用将被返回,取代新建的对象。

public class Singleton implements Serializable {

    private static Singleton INSTANCE= new Singleton();
    private Singleton(){};
    
    private Object readResolve(){
        return INSTANCE;
    }
}
相关文章
相关标签/搜索