Java Dev 5 - Serialization & Deserialization

Lời nói đầu
Bài này tôi sẽ note lại 1 số thứ về Serialization & Deserialization trong Java. Nếu muốn tìm hiểu chi tiết hơn thì bạn có thể tìm đọc các link tham khảo ở cuối bài.
Tổng quan

Serialization là cơ chế chuyển đổi cấu trúc dữ liệu (data structure) hoặc trạng thái của object (object states) trở thành luồng byte (byte streams), byte streams có thể được lưu vào file, bộ nhớ đệm (memory buffering), database hoặc truyền đi qua network.
Deserialization là cơ chế ngược lại với Serialization, cho phép khôi phục lại object states thông qua việc phân tích byte streams được tạo ra sau khi Serialization.
Nhờ cơ chế này mà object states có thể được lưu trữ ở trong cùng 1 môi trường hay cả 2 môi trường khác nhau. Cơ chế này được sử dụng rộng rãi trên các công nghệ như EJB, RMI, Hessian.
Lý do cần Serialization
Các định dạng như JSON, XML chỉ cho phép lưu các kiểu dữ liệu cơ bản. Trong khi đó Serialization hỗ trợ việc lưu trữ và truyền tải các đối tượng phức tạp, bao gồm cả các quan hệ đối tượng, kế thừa, đa hình và các tính năng nâng cao khác, đặc biệt trong ngôn ngữ Java.
Serialization
Để hiểu cách Java serializes 1 object, ta xem xét ví dụ sau:
// Class SerializationDemo được implements từ interface Serializable.
public class SerializationDemo implements Serializable {
private String stringField;
private int intField;
public SerializationDemo(String s, int i) {
this.stringField = s;
this.intField = i;
}
public static void main(String[] args) throws IOException {
// Tạo đối tượng từ class SerializationDemo
SerializationDemo object = new SerializationDemo("gyyyy", 97777);
/*
Khởi tạo 1 object từ class ByteArrayOutputStream
việc này giống như tạo ra 1 vùng để chứa các byte,
vùng chứa này là 1 mảng byte (byte array)
*/
ByteArrayOutputStream bout = new ByteArrayOutputStream();
/*
Khởi tạo object từ class ObjectOutputStream,
ObjectOutputStream có các method dùng để serialize,
khi serialize thì dữ liệu sẽ được ghi vào 1 OutputStream.
Vậy nên ta truyền vào object từ class ByteArrayOutputStream
để làm vùng chứa
*/
ObjectOutputStream out = new ObjectOutputStream(bout);
// method writeObject() được dùng để serialize đối tượng
out.writeObject(object);
}
}
Ở ví dụ trên, ta tiến hành serialize một object kiểu SerializationDemo.
Một vài lưu ý:
Chỉ các object của các class được
implementstừ interfaceSerializablemới có thể được đưa vào quá trình Serialization.Nếu lớp cha đã được
implementstừ interfaceSerializablethì lớp con không cầnimplementsvẫn có thể được đưa vào quá trình Serialization.Bên trong class, các dữ liệu có non-access modifier là
staticvàtransientsẽ không được lưu vào object states khi serialize.Bên trong class, các dữ liệu có non-access modifier là
finalsẽ luôn được lưu vào object states khi serialize. Do đó việc 1 dữ liệu có cả 2 non-access modifier làfinalvàstaticthì vẫn luôn được lưu vào object states. Với ví dụtransient final int age = 20, sau khi serialize thìagevẫn có giá trị20.
Sau khi serialize, sử dụng các công cụ hex-editor ta có thể thấy dữ liệu là dạng chuỗi nhị phân (binary string):
ac ed 00 05 73 72 00 11 53 65 72 69 61 6c 69 7a ....sr.. Serializ
61 74 69 6f 6e 44 65 6d 6f d9 35 3c f7 d6 0a c6 ationDem o.5<....
d5 02 00 02 49 00 08 69 6e 74 46 69 65 6c 64 4c ....I..i ntFieldL
00 0b 73 74 72 69 6e 67 46 69 65 6c 64 74 00 12 ..string Fieldt..
4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e Ljava/la ng/Strin
67 3b 78 70 00 01 7d f1 74 00 05 67 79 79 79 79 g;xp..}. t..gyyyy
0xaced: STREAM_MAGIC. Chỉ ra đây là dữ liệu dạng serialize.
Thật ra tôi định phân tích nốt các giá trị hex còn lại, nhưng nó khá dài (tương lai tôi có thể sẽ update phần này). Chi tiết tham khảo tại đây: https://docs.oracle.com/javase/8/docs/api/java/io/ObjectStreamConstants.html
Thông tin thêm: 1 dấu hiệu nữa của 1 serialized data trong Java là khi được chuyển sang dạng Base64 thì sẽ có phần đầu chuỗi là
rO0AB
Deserialization
Xem xét ví dụ sau, đây là phần nối tiếp với ví dụ trước:
public static void main(String[] args) throws ClassNotFoundException {
// khai báo 1 mảng byte, giả sử data này là output từ việc serialize
// trong ví dụ trước
byte[] data;
/*
Ở trên mình đã nói khi serialize thì data sẽ được ghi vào 1 OutputStream.
ByteArrayInputStream chứa Internal buffer chứa các byte
có thể được đọc từ stream.
Internal buffer theo dõi byte tiếp theo được cung cấp từ method readObject()
*/
ByteArrayInputStream bin = new ByteArrayInputStream(data);
/*
Khởi tạo object từ class ObjectInputStream,
ObjectInputStream có các method dùng để deserialize
*/
ObjectInputStream in = new ObjectInputStream(bin);
// method readObject() được dùng để deserialize đối tượng
SerializationDemo demo = (SerializationDemo) in.readObject();
}
Ở ví dụ vừa xong, ta tiến hành deserialize một object kiểu SerializationDemo.
Một số lưu ý:
- Khi 1 object được deserialized, constructor của object đó sẽ không bao giờ được gọi.
SerialVersionUID
Serialization runtime liên kết với mỗi class Serializable một số phiên bản (version number), version number này còn gọi SerialVersionUID hay viết tắt là SUID.
SUID chiếm 1 vị trí rất quan trọng trong quá trình Serialization. Nó tương đương với thông tin dấu vân tay (fingerprint) của một object và có thể trực tiếp xác định sự thành công của quá trình Serialize.
Nếu bên nhận (receiver) đã load một class (giả sử class A) cho object có SUID khác với class tương ứng (cũng là class A) của bên gửi (sender) thì khi deserialize sẽ dẫn đến InvalidClassException.
Một vài lưu ý:
Một class có thể Serialize (Serializable) có thể khai báo rõ ràng UID của chính nó bằng cách khai báo tên trường (Field name) là
serialVersionUID. Field này phải làstaticvà có kiểu dữ liệulong.
Ví dụ:<Access Modifier bất kì> static long serialVersionUID=42L;Nếu một class (Serializable) không khai báo serialVersionUID một cách rõ ràng, thì Serialization runtime tự sẽ tính toán một giá trị mặc định cho class đó dựa trên các khía cạnh khác nhau của class.
Kết bài
Bài này có vẻ khá ngắn nhưng cũng làm tôi mất khá nhiều thời gian để viết. Sang bài sau tôi sẽ giới thiệu về Reflection API. Việc hiểu cơ chế Serialization/Deserialization và Reflection API sẽ là bước đệm cho việc hiểu và phân tích lỗ hổng Insecure Deserialization trong Java.




