You don’t know the secret of Java object serialization

brief introduction

Did you know that serialization can use proxies? Do you know the security of serialization? Every java programmer has heard of serialization. To store objects, you need to serialize them. To transmit objects on the network, you need to serialize them. It looks very simple. In fact, there are many small secrets hidden in serialization. Today, this article will reveal them one by one.

More highlights:

What is serialization

Serialization is to organize Java objects in a certain order for transmission on the network or writing to storage. Deserialization is to read the stored object from the network or storage and convert it into a real Java object.

Therefore, the purpose of serialization is to transfer objects. For some complex objects, we can use the excellent framework of a third party, such as thrift, protocol buffer, etc., which is very convenient to use.

The JDK itself also provides serialization. To make an object serializable, you can implement Java io. Serializable interface.

java. io. Serializable is from jdk1 1, which is actually a marker interface, because Java io. Serializable has no interface to implement. Inherit Java io. Serializable indicates that the class object can be serialized.

@Data
@AllArgsConstructor
public class CustUser implements java.io.Serializable{
    private static final long serialVersionUID = -178469307574906636L;
    private String name;
    private String address;
}

Above, we defined a custuser serializable object. This object has two properties: name and address.

Next, let's look at how to serialize and deserialize:

public void testCusUser() throws IOException,ClassNotFoundException {
        CustUser custUserA=new CustUser("jack","www.flydean.com");
        CustUser custUserB=new CustUser("mark","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
            objectOutputStream.writeObject(custUserB);
        }
        
        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUser custUser1 = (CustUser) objectInputStream.readObject();
            CustUser custUser2 = (CustUser) objectInputStream.readObject();
            log.info("{}",custUser1);
            log.info("{}",custUser2);
        }
    }

In the above example, we instantiated two custuser objects, wrote the objects to the file using objectoutputstream, and finally read the objects from the file using objectinputstream.

The above is the most basic use. Note that there is a serialVersionUID field in custuser class.

SerialVersionUID is the unique mark of serialized objects. If the serialVersionUID defined in class is consistent with the serialVersionUID in serialization storage, it indicates that the two objects are one object. We can deserialize the stored objects.

If we do not have a serialVersionUID defined in the display, the JVM will automatically generate it according to the fields, methods and other information in the class. Many times when I look at the code, I find that many people set the serialVersionUID to 1L, which is wrong because they don't understand the real meaning of serialVersionUID.

Refactoring serialized objects

If we have a serialized object in use, but suddenly we find that this object seems to be missing a field. If we want to add it, can we add it? After adding it, can the original serialized object be converted into this new object?

The answer is yes, provided that the serialVersionUID of the two versions must be the same. The newly added field is null after deserialization.

Serialization is not encryption

Many students may think that serialization has turned the object into a binary file. Does it mean that the object has been encrypted?

This is actually a misunderstanding of serialization. Serialization is not encryption, because even if you serialize, you can still know the structure of your class from the data after serialization. For example, in the environment of RMI remote call, even the private field in class can be parsed from the stream stream.

What if we want to encrypt some fields during serialization?

At this time, you can consider adding writeobject and readObject methods to the serialized object:

private String name;
    private String address;
    private int age;

    private void writeObject(ObjectOutputStream stream)
            throws IOException
    {
        //给age加密
        age = age + 2;
        log.info("age is {}",age);
        stream.defaultWriteObject();
    }

    private void readObject(ObjectInputStream stream)
            throws IOException,ClassNotFoundException
    {
        stream.defaultReadObject();
        log.info("age is {}",age);
        //给age解密
        age = age - 2;
    }

In the above example, we added an age object for custuser, encrypted the age in writeobject (plus 2) and decrypted the age in readObject (minus 2).

Note that both writeobject and readObject are private void methods. Their calls are implemented through reflection.

Use real encryption

In the above example, we only encrypt the age field. If we want to encrypt the whole object, is there any good way?

JDK provides us with javax crypto. Sealedobject and Java security. Signedobject as the encapsulation of the serialized object. This encrypts the entire serialized object.

Another example:

public void testCusUserSealed() throws IOException,ClassNotFoundException,NoSuchPaddingException,NoSuchAlgorithmException,IllegalBlockSizeException,BadPaddingException,InvalidAlgorithmParameterException,InvalidKeyException {
        CustUser custUserA=new CustUser("jack","www.flydean.com");
        Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(),"AES");
        IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes());
        enCipher.init(Cipher.ENCRYPT_MODE,secretKey,iv);
        deCipher.init(Cipher.DECRYPT_MODE,iv);
        SealedObject sealedObject= new SealedObject(custUserA,enCipher);

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(sealedObject);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            SealedObject custUser1 = (SealedObject) objectInputStream.readObject();
            CustUser custUserV2= (CustUser) custUser1.getObject(deCipher);
            log.info("{}",custUserV2);
        }
    }

In the above example, we build a sealedobject object and the corresponding encryption and decryption algorithm.

Sealedobject is like a proxy. What we write and read are the encrypted objects of the proxy. So as to ensure the security in the process of data transmission.

Use agent

The sealedobject above is actually a proxy. Consider this situation. If there are many fields in the class and these fields can be automatically generated from one of them, we don't need to serialize all the fields. We just serialize that field, and other fields can be derived from this field.

In this case, we need to use the proxy function of serializing objects.

First, the serialized object needs to implement the writereplace method to replace it with the object you really want to write:

public class CustUserV3 implements java.io.Serializable{

    private String name;
    private String address;

    private Object writeReplace()
            throws java.io.ObjectStreamException
    {
        log.info("writeReplace {}",this);
        return new CustUserV3Proxy(this);
    }
}

Then, in the proxy object, you need to implement the readresolve method to reconstruct the serialized object from the serialized data. As follows:

public class CustUserV3Proxy implements java.io.Serializable{

    private String data;

    public CustUserV3Proxy(CustUserV3 custUserV3){
        data =custUserV3.getName()+ "," + custUserV3.getAddress();
    }

    private Object readResolve()
            throws java.io.ObjectStreamException
    {
        String[] pieces = data.split(",");
        CustUserV3 result = new CustUserV3(pieces[0],pieces[1]);
        log.info("readResolve {}",result);
        return result;
    }
}

Let's see how to use:

public void testCusUserV3() throws IOException,ClassNotFoundException {
        CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject();
            log.info("{}",custUser1);
        }
    }

Note that we write and read custuserv3 objects.

The difference between serializable and externalizable

Finally, let's talk about the difference between externalizable and serializable. Externalizable inherits from serializable. It needs to implement two methods:

 void writeExternal(ObjectOutput out) throws IOException;
 void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;

When do I need to use writeexternal and readexternal?

Using serializable, Java will automatically serialize the objects and fields of the class, which may take up more space. Externalizable requires us to control how to write / read, which is troublesome. However, if performance is considered, externalizable can be used.

In addition, serializable does not need to execute a constructor to deserialize. Externalizable needs to execute the constructor to construct the object and then call the readExternal method to populate the object. So an externalizable object needs a parameterless constructor.

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>