java序列化知多少

java序列化知多少VO类实现序列化,然后缓存到redis里,在这种情况下,一般来说是问题不大的,因为我们的VO类等同于逻辑内容,VO类基本上是一些属性值和get/

大家好,欢迎来到IT知识分享网。

1 序列化概念

大多数小伙伴肯定知道什么是序列化啦!也不用我多说。序列化主要有两个作用:对象持久化网络间传输。对象持久化例如,把对象转换成二进制流写入到磁盘上。网络间传输,在rpc框架间用的很广泛。

  • 序列化:把对象编码成二进制流的过程叫做序列化。
  • 反序列化:把对象二进制流转换为对象的过程叫做反序列化。

2 如何运用好序列化

“如何运用好序列化”看到这个标题你会很诧异,”呵,这还不简单,实现Serializable接口不就完事了吗,很难吗?”三年前我那会刚毕业出来找工作的时候,面试官问我”你是如何实现序列化的”,我:“这还不简单吗?实现Serializable接口不就完了吗?” 结果可想而知 。现在回想为什么我当时会这样认为呢?大多数情况下,是从数据库查出数据,转换成VO返回到前端页面就完事了。VO类实现序列化,然后缓存到redis里,在这种情况下,一般来说是问题不大的,因为我们的VO类等同于逻辑内容,VO类基本上是一些属性值和get/set方法,比如人名,手机号,地址等,这些属性本来就是API接口要提供的,所以暴露出去不成问题。

实现序列化接口的代价:

破坏了了对象的封装性。如果我们只实现了Serializable接口,类中的所有实例域都会被导出,包括私有的属性,类设计的目标是“最低限度的访问域”,类里面的一些自有的域对访问者是不可直接访问的。

增加了bug和出现安全漏洞的可能性

序列化机制是一种语言之外的的对象创建机制,反序列化机制是一个“隐藏的构造器”,具备与其它构造器相同的特点。

  • java的默认的序列化方式码流十分庞大,性能十分差。
  • java默认的序列化方式不支持跨语言和跨平台。

关于第三点和第四点以后再说。

3 是否需要序列化的一些准则

  • 为了继承而设计的类,应该尽可能的少实现Serializable接口。
  • 如果父类不是可序列化的,那么也无法编写可序列化的子类。特别是,父类在没有提供无参构造器的情况下。
  • – 在“允许子类序列化”和“不允许子类序列化”之间存在一个折中的方案,父类提供一个无参的构造器,由子类自己决定是否实现序列化。

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

为什么说“为了继承而设计的类,应该尽可能的少实现Serializable接口”呢。如果父类实现了序列化,那么子类中的属性也会被序列化,因此程序员在开发子类的时候,就必须考虑哪些东西不该暴露出去,序列化会暴露对象内部的实例域,包括私有的实例域,因此这会增加开发人员的负担。

3.2 如果父类不是可序列化的,那么也无法编写可序列化的子类。特别是,父类在没有提供无参构造器的情况下。

“如果父类不是可序列化的,那么也无法编写可序列化的子类。特别是,父类在没有提供无参构造器的情况下”。如果我们专为继承实现了一些类,这里类需要子类继承并实现,而我们开发的子类又有序列化的要求,这是如果父类不可序列化,那么我们无法编写可序列化的子类,并不是说会报错,而是反序列化后父类中的实例域都会丢失,只有在父类没有提供无参构造器的情况下,才会反序列化失败报错。以下代码说明这一情况:

Parent是为了继承而设计的抽象类,需要子类自己继承该类并实现该类的抽象方法(该示例代码中没有体现,也不需要),下面的Parent类没有实现序列化接口,也就不是可序列化的,父类的属性在序列化的时候会丢失。

public abstract class Parent {

	private String name;

	private Date date;

	public Parent() {

		this.name = name;

	}

	public Parent(String name) {

		this.name = name;

	}

	public Parent(String name, Date date) {

		this.name = name;

		this.date = date;

	}

	public String getName() {

		return name;

	}

	public void setName(String name) {

		this.name = name;

	}

	public Date getDate() {

		return date;

	}

	public void setDate(Date date) {

		this.date = date;

	}

}

下面我们有一个Child类,继承了抽象父类Parent,实现了父类的一些方法以满足需求(该类并没有也不需要体现,我们的主要关注点在序列化上)。

public class Child extends Parent implements Serializable {

    private static final long serialVersionUID = 1L;

    private String sex;

    @Override

    public String toString() {

        return "Child{" + "sex='" + sex + '\'' + ", name='" + super.getName() + '\'' + ", date=" + super.getDate()

            + '}';

    }

    public String getSex() {

        return sex;

    }

    public void setSex(String sex) {

        this.sex = sex;

    }

    public Child() {

        super("我是你爸爸", new Date());

    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        Child child = new Child();

        child.setSex("男");

        System.out.println("序列化之前对象状态:");

        System.out.println(child.toString());

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

        objectOutputStream.writeObject(child);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);

        Child deserier = (Child)objectInputStream.readObject();

        System.out.println("反序列化之后对象状态:");

        System.out.println(deserier);

    }

}

运行结果:

java序列化知多少

3.2 运行结果

可以看到父类实例域相关的值都是空的,如果我们的子类实现过程中依赖于这些状态值,但又取不到值,那么仅子类序列化是无意义的。

假如我们去掉父类的无参构造器,尽管子类调用的是父类的有参构造器,反序列化也不会成功,运行时会报InvalidClassException异常,提示没有有效的构造器。

java序列化知多少

无空构造器情况下报错

3.3 在“允许子类序列化”和“不允许子类序列化”的折中方案

基于3.1和3.2的讨论,我们在设计抽象类的时候,并不确定子类需不要序列化,因此通常我们需要提供无参的构造器,由子类自己决定时候需要序列化,子类如果需要序列化,所做的工作只是实现Serialible接口,编写readObject 和writeObject方法,视情况决定自己需要序列化或者反序列化哪些属性。示例代码:

public class Child extends Parent implements Serializable {

    private static final long serialVersionUID = 1L;

    private String sex;

// 与3.2相比多了这一个方法

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

        inputStream.defaultReadObject();

    }

    @Override

    public String toString() {

        return "Child{" + "sex='" + sex + '\'' + ", name='" + super.getName() + '\'' + ", date=" + super.getDate()

            + '}';

    }

    public String getSex() {

        return sex;

    }

    public void setSex(String sex) {

        this.sex = sex;

    }

    public Child() {

        super("我是你爸爸", new Date());

    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        Child child = new Child();

        child.setSex("男");

        System.out.println("序列化之前对象状态:");

        System.out.println(child.toString());

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

        objectOutputStream.writeObject(child);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);

        Child deserier = (Child)objectInputStream.readObject();

        System.out.println("反序列化之后对象状态:");

        System.out.println(deserier);

    }

    public static String bytesToHexString(byte[] bytes) {

        if (bytes == null)

            return null;

        StringBuffer stringBuffer = new StringBuffer(bytes.length * 2);

        for (byte b : bytes) {

            if ((b & 0xFF) < 0x10) {

                stringBuffer.append('0');

            }

            stringBuffer.append(Integer.toHexString(0xFF & b));

            stringBuffer.append(" ");

        }

        return stringBuffer.toString().toUpperCase(Locale.getDefault());

    }

运行结果:

java序列化知多少

3.3 运行结果

可以看到相比3.2,反序列化之后,name和date实例域也是有值的。

4 序列化安全性问题

4.1 序列化规范

在探讨如何保证序列化安全性问题之前,我们先看一个例子。在演示这个例子之前,我还得介绍些关于序列化协议的知识。如何让你来从底层实现序列化协议,那么你肯定会考虑如何去描述一个对象,需要有一种基础结构描述对象的类,如果是动态代理类如何处理、对象的属性、对象的引用、引用类型、空对象如何描述、循环引用怎么处理,重复引用某个对象怎么处理等等,这些东西还是很复杂的。JDK官方有一套序列化语法规则:

stream:

  magic version contents

contents:

  content

  contents content

content:

  object

  blockdata

object:

  newObject

  newClass

  newArray

  newString

  newEnum

  newClassDesc

  prevObject

  nullReference

  exception

  TC_RESET

newClass:

  TC_CLASS classDesc newHandle

classDesc:

  newClassDesc

  nullReference

  (ClassDesc)prevObject      // an object required to be of type

                             // ClassDesc

superClassDesc:

  classDesc

newClassDesc:

  TC_CLASSDESC className serialVersionUID newHandle classDescInfo

  TC_PROXYCLASSDESC newHandle proxyClassDescInfo

classDescInfo:

  classDescFlags fields classAnnotation superClassDesc 

className:

  (utf)

serialVersionUID:

  (long)

classDescFlags:

  (byte)                  // Defined in Terminal Symbols and

                            // Constants

proxyClassDescInfo:

  (int)<count> proxyInterfaceName[count] classAnnotation

      superClassDesc

proxyInterfaceName:

  (utf)

fields:

  (short)<count>  fieldDesc[count]

fieldDesc:

  primitiveDesc

  objectDesc

primitiveDesc:

  prim_typecode fieldName

objectDesc:

  obj_typecode fieldName className1

fieldName:

  (utf)

className1:

  (String)object             // String containing the field's type,

                             // in field descriptor format

classAnnotation:

  endBlockData

  contents endBlockData      // contents written by annotateClass

prim_typecode:

  `B'       // byte

  `C'       // char

  `D'       // double

  `F'       // float

  `I'       // integer

  `J'       // long

  `S'       // short

  `Z'       // boolean

obj_typecode:

  `[`   // array

  `L'       // object

newArray:

  TC_ARRAY classDesc newHandle (int)<size> values[size]

newObject:

  TC_OBJECT classDesc newHandle classdata[]  // data for each class

classdata:

  nowrclass                 // SC_SERIALIZABLE & classDescFlag &&

                            // !(SC_WRITE_METHOD & classDescFlags)

  wrclass objectAnnotation  // SC_SERIALIZABLE & classDescFlag &&

                            // SC_WRITE_METHOD & classDescFlags

  externalContents          // SC_EXTERNALIZABLE & classDescFlag &&

                            // !(SC_BLOCKDATA  & classDescFlags

  objectAnnotation          // SC_EXTERNALIZABLE & classDescFlag&& 

                            // SC_BLOCKDATA & classDescFlags

nowrclass:

  values                    // fields in order of class descriptor

wrclass:

  nowrclass

objectAnnotation:

  endBlockData

  contents endBlockData     // contents written by writeObject

                            // or writeExternal PROTOCOL_VERSION_2.

blockdata:

  blockdatashort

  blockdatalong

blockdatashort:

  TC_BLOCKDATA (unsigned byte)<size> (byte)[size]

blockdatalong:

  TC_BLOCKDATALONG (int)<size> (byte)[size]

endBlockData   :

  TC_ENDBLOCKDATA

externalContent:          // Only parseable by readExternal

  ( bytes)                // primitive data

    object

externalContents:         // externalContent written by 

  externalContent         // writeExternal in PROTOCOL_VERSION_1.

  externalContents externalContent

newString:

  TC_STRING newHandle (utf)

  TC_LONGSTRING newHandle (long-utf)

newEnum:

  TC_ENUM classDesc newHandle enumConstantName

enumConstantName:

  (String)object

prevObject

  TC_REFERENCE (int)handle

nullReference

  TC_NULL

exception:

  TC_EXCEPTION reset (Throwable)object         reset 

magic:

  STREAM_MAGIC

version

  STREAM_VERSION

values:          // The size and types are described by the

                 // classDesc for the current object

newHandle:       // The next number in sequence is assigned

                 // to the object being serialized or deserialized

reset:           // The set of known objects is discarded

                 // so the objects of the exception do not

                 // overlap with the previously sent objects 

                 // or with objects that may be sent after 

                 // the exception

备注: 如何看懂这套语法图呢?首先冒号左边的是解释说明,右边的才是流里面的内容,看懂这副语法图,我们需要用递归的思维去看,比如在语法树开头部分我们读到了“magic”,那么我们继续向下搜索“maigic”,直到找到终端标志(可以理解为语法规则里的常量)为止,在这里我们读到STREAM_MAGICz终端标志,查看终端标志表,发现是magic魔数,声明使用序列化协议,没有太大含义,就跟class文件以CAFEBABY开头一样。

所有的终端标志如下:

final static short STREAM_MAGIC = (short)0xaced;

    final static short STREAM_VERSION = 5;

    final static byte TC_NULL = (byte)0x70;

    final static byte TC_REFERENCE = (byte)0x71;

    final static byte TC_CLASSDESC = (byte)0x72;

    final static byte TC_OBJECT = (byte)0x73;

    final static byte TC_STRING = (byte)0x74;

    final static byte TC_ARRAY = (byte)0x75;

    final static byte TC_CLASS = (byte)0x76;

    final static byte TC_BLOCKDATA = (byte)0x77;

    final static byte TC_ENDBLOCKDATA = (byte)0x78;

    final static byte TC_RESET = (byte)0x79;

    final static byte TC_BLOCKDATALONG = (byte)0x7A;

    final static byte TC_EXCEPTION = (byte)0x7B;

    final static byte TC_LONGSTRING = (byte) 0x7C;

    final static byte TC_PROXYCLASSDESC = (byte) 0x7D;

    final static byte TC_ENUM = (byte) 0x7E;

    final static  int   baseWireHandle = 0x7E0000;

示例:

public class List implements java.io.Serializable {

	int value;

	List next;

	public static void main(String[] args) {

		try {

			List list1 = new List();

			List list2 = new List();

			list1.value = 17;

			list1.next = list2;

			list2.value = 19;

			list2.next = null;

			FileOutputStream o = new FileOutputStream("1.txt");

			ObjectOutputStream out = new ObjectOutputStream(o);

			out.writeObject(list1);

			out.writeObject(list2);

			out.flush();

		} catch (Exception ex) {

			ex.printStackTrace();

		}

	}

}

打开文件1.txt:

aced 0005 7372 0012 636f 6d2e 6769 6d63

2e74 6573 742e 4c69 7374 4127 d6e5 a5d0

5a8d 0200 0249 0005 7661 6c75 654c 0004

6e65 7874 7400 144c 636f 6d2f 6769 6d63

2f74 6573 742f 4c69 7374 3b78 7000 0000

1173 7100 7e00 0000 0000 1370 7100 7e00

03

4.2 演示

废话不多说,现在演示安全性攻击问题:

Parent:这是我们父类实现了序列化接口 包含name属性和date属性

public class Parent implements Serializable {

	private String name;

	private Date date;

	// 此处省略所有的构造方法

	// 此处省略protected 的get/set方法

	//set date私有

	private void setDate(Date date) {

		this.date = date;

	}

}
public class Child  extends Parent implements Serializable {

	private static final long serialVersionUID = 1L;

	private String sex;

    // 此处省略get/set方法

	public Child() {

		super("我是你爸爸", new Date());

	}

	@Override

	public String toString() {

		return "Child{" + "sex='" + sex + '\'' + ", name='" + super.getName() + '\'' + ", date=" + super.getDate()

				+ '}';

	}

	

	public static void main(String[] args) throws IOException, ClassNotFoundException {

		Child child = new Child();

		child.setSex("男");

		System.out.println("序列化之前对象状态: " + child);

		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

		ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

		objectOutputStream.writeObject(child);

		byte[] ref = {0x71, 0, 0x7e, 0, 6};

		byteArrayOutputStream.write(ref);

		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());

		ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);

		Child deserier = (Child)objectInputStream.readObject();

		System.out.println("反序列化之后对象状态: " + deserier);

		// 拿到反序列化对象中的内部对象

		Date date = (Date)objectInputStream.readObject();

		System.out.println(date);

		date.setYear(110);

		System.out.println(deserier);

	}

}

Child是我们子类继承Parent并且实现了序列化接口,main方法中首先我们构造了合法的字节流,接着我们伪造字节流,插入“0x71, 0, 0x7e, 0, 6”,查询语法表可知,0x71 含义如下:

prevObject

  TC_REFERENCE (int)handle

代表这是一个引用,指向之前的一个对象,为了能够方便的找到之前的对象,序列化引入了handle的概念,类似于句柄,handle是32位的整型,通过该值能够迅速找到之前的对象,handle起始值必须从0x7E0000 开始。在这个例子中我们的handle值是0x007e0006,可以理解为引用的是第六号“对象”,这里的“对象”不完全是域对象,也指代域对象的描述符,比如“Ljava/lang/String”。

A basic structure is needed to represent objects in a stream. Each attribute of the object needs to be represented: its classes, its fields, and data written and later read by class-specific methods. The representation of objects in the stream can be described with a grammar. There are special representations for null objects, new objects, classes, arrays, strings, and back references to any object already in the stream. Each object written to the stream is assigned a handle that is used to refer back to the object. Handles are assigned sequentially starting from 0x7E0000. The handles restart at 0x7E0000 when the stream is reset.

运行结果:

序列化之前对象状态: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:39:25 GMT+08:00 2020}

反序列化之后对象状态: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:39:25 GMT+08:00 2020}

拿到反序列化对象内部的私有属性Sat May 23 15:39:25 GMT+08:00 2020

Child{sex='男', name='我是你爸爸', date=Sun May 23 15:39:25 GMT+08:00 2010}

查看运行结果可以清楚的看到,通过伪造流,我们拿到了对象的私有域,并且改变了私有域的状态,date本应该是2020年的,却被我们强行改造为2010年,这是很严重的安全问题。

要解决这个问题也很简单,在Parent父类里编写保护性的readObject()方法。

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

		inputStream.defaultReadObject();

		date = new Date(date.getTime());

	}

改造后的运行结果:

序列化之前对象状态: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:44:33 GMT+08:00 2020}

反序列化之后对象状态: Child{sex='男', name='我是你爸爸', date=Sat May 23 15:44:33 GMT+08:00 2020}

拿到反序列化对象内部的私有属性Sat May 23 15:44:33 GMT+08:00 2020

Child{sex='男', name='我是你爸爸', date=Sat May 23 15:44:33 GMT+08:00 2020}

5 关于序列化的安全性问题建议

5.1 反序列化时候别忘记校验对象的有效性

当我们反序列化的时候,会从对象流读取字节码,构造新的对象,因此反序列化的时候,我们也要保证反序列化的对象和正常new出来的对象具有相同的约束,这样做的目的是防止手工篡改过的流成功反序列化成“不合法”的对象,通常这样的对象的内部状态并不是我们期望的。因此,在readObject()方法里,我们需要手动校验对象的状态是否合法。如果有效性校验失败,我们需要果断抛出InvalidObjectException。

5.2 readObject方法的保护性

如果你觉得校验了对象的有效性,就可以规避安全问题,那么你就错了,如上所述4.2

5.3 强烈建议不要序列化内部类(即非静态成员类的嵌套类)

因为在非静态上下文中声明的内部类包含对封闭类实例的隐式非瞬态引用,所以序列化此类内部类实例也将导致对其关联的外部类实例进行序列化。由javac(或其他Java TM编译器)生成的用于实现内部类的合成字段取决于实现,并且在编译器之间可能有所不同。这些字段的差异可能会破坏兼容性,并导致默认冲突serialVersionUID价值观。分配给本地和匿名内部类的名称也取决于实现,并且在编译器之间可能有所不同。由于内部类不能声明除编译时常量字段以外的静态成员,因此内部类不能使用该 serialPersistentFields机制来指定可序列化的字段。最后,由于与外部实例相关联的内部类没有零参数构造函数(此类内部类的构造函数隐式接受封闭实例作为前置参数),因此它们无法实现 Externalizable。但是,上面列出的所有问题均不适用于静态成员类

5.4 readObject方法第一行最好先调用inputStream.defaultReadObject()方法;

这是为了保证前后版本的兼容性,defaultReadObject会帮我们给对象的非transient字段默认赋值。加入我们升级了版本在第二个版本中新增了某个属性,而且上一个版本没有调用默认的defaultReadObject,最新版本的字节流在上一个版本发序列化,那么反序列化会失败。

5.5 单例控制的类不要实现序列化接口。

单例控制的类不要实现序列化接口,如果实现了,反序列化的时候实际上就不再是单例了,每次反序列化都会创建一个新的实例。

5.6 考虑使用序列化代理模式

序列化代理实际上就是一个实现了序列化接口的静态内部类,并且和外部类具有相同的参数,然后在外部类中重写writeReplace(),通过代理类去做序列化,反序列化的时候使用readResolve方法。(该方法已经不被官方推荐了)

个人不太看好序列化代理模式,所以也没有详细代码介绍该模式。

一是写起来很繁琐;二是破坏了兼容性,如果将来外部类需要被子类重写,那么就不可能和子类兼容。好处是防止伪字节流攻击和内部敏感域盗用。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/13095.html

(0)
上一篇 2024-03-08 17:15
下一篇 2024-03-08 17:33

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信