/ 写在前面 /
好久没更新新了,后面还会持续更新,至少每周一篇🥰🥰
需要进群的小伙伴私信就行了,可能回复不及时,看到会回复。
视频教程:https://www.bilibili.com/video/BV1KT421k7ZE/
/ 序列化和反序列化概念 /
Java 序列化是指把 Java 对象转换为字节序列的过程,而 Java 反序列化是指把字节序列恢复为 Java 对象的过程:
1、序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。
2、反序列化:客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
/ 为什么需要序列化与反序列化? /
对象序列化可以实现分布式对象。
主要应用例如:RMI(即远程调用 Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
java 对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。
可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
序列化可以将内存中的类写入文件或数据库中。
比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。
/ 序列化和反序列化的实现 /
JDK 类库提供的序列化 API
java.io.ObjectOutputStream:表示对象输出流它的 writeObject(Object obj)方法可以对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream:表示对象输入流它的 readObject()方法从源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回。
只有实现了 Serializable 或 Externalizable 接口的类的对象才能被序列化,否则抛出异常。
实现 Java 对象序列化与反序列化的方法
假定一个 Student 类,它的对象需要序列化,可以有如下三种方法:
方法一:若 Student 类仅仅实现了 Serializable 接口,则可以按照以下方式进行序列化和反序列化。ObjectOutputStream 采用默认的序列化方式,对 Student 对象的非 transient 的实例变量进行序列化。ObjcetInputStream 采用默认的反序列化方式,对对 Student 对象的非 transient 的实例变量进行反序列化。
public static void serial() throws IOException {
Student llu = new Student("llu", 18);
System.out.println(llu);
File file = new File("llu.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(llu);
}
// 反序列化
public static void deserial() throws Exception {
File file = new File("llu.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Student llu = (Student) objectInputStream.readObject();
System.out.println(llu);
}
方法二:若 Student 类仅仅实现了 Serializable 接口,并且还定义了 readObject(ObjectInputStream in)和 writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。ObjectOutputStream 调用 Student 对象的 writeObject(ObjectOutputStream out)的方法进行序列化。
public class Student implements java.io.Serializable {
private String name;
private int age;
// 对象在序列化的时候调用
private void writeObject(ObjectOutputStream fos) throws IOException {
fos.writeObject(this);
Runtime.getRuntime().exec("calc");
}
//对象在反序列化的时候调用
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.readObject();
}
}
方法三:若 Student 类实现了 Externalnalizable 接口,且 Student 类必须实现 readExternal(ObjectInput in)和 writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。ObjectOutputStream 调用 Student 对象的 writeExternal(ObjectOutput out))的方法进行序列化。ObjectInputStream 会调用 Student 对象的 readExternal(ObjectInput in)的方法进行反序列化。
序列化的必要条件
1、必须是同包,同名。
2、serialVersionUID 必须一致。有时候两个类的属性稍微不一致的时候,可以通过将此属性写死值,实现序列化和反序列化。
如果序列化和反序列化的的类的 UID 不同,则会抛出 InvalidClassException 异常。
序列化 ID 问题
简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。
在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体(类)的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
当实现 java.io.Serializable 接口的实体(类)没有显式地定义一个名为 serialVersionUID,类型为 long 的变量时,Java 序列化机制会根据编译的 class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况下,只有同一次编译生成的 class 才会生成相同的 serialVersionUID 。
特性使用案例
读者应该听过 Façade 模式,它是为应用程序提供统一的访问接口,案例程序中的 Client 客户端使用了该模式,案例程序结构图下图所示。
Client 端通过 Façade Object 才可以与业务逻辑对象进行交互。而客户端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通过网络将二进制对象数据传给 Client,Client 负责反序列化得到 Façade 对象。该模式可以使得 Client 端程序的使用需要服务器端的许可,同时 Client 端和服务器端的 Façade Object 类需要保持一致。当服务器端想要进行版本更新时,只要将服务器端的 Façade Object 类的序列化 ID 再次生成,当 Client 端反序列化 Façade Object 就会失败,也就是强制 Client 端从服务器端获取最新程序。
对敏感字段加密
情景:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,下面的代码展示了这个过程。
private void writeObject(ObjectOutputStream fos) throws IOException {
try {
// 获取对象流的字段存储对象,用于显式设置对象的字段值
ObjectOutputStream.PutField putFields = fos.putFields();
// 打印原始密码
System.out.println("原密码:" + this.phone);
// 模拟密码加密过程,将原始密码修改为加密后的密码
this.phone = "encryption";
// 在字段存储对象中为password字段设置加密后的密码值
putFields.put("phone", this.phone);
putFields.put("name",this.name);
putFields.put("age",this.age);
// 打印加密后的密码
System.out.println("加密后的密码" + this.phone);
// 将字段存储对象中的数据写入到流中
fos.writeFields();
} catch (IOException e) {
// 捕获并处理IO异常
e.printStackTrace();
}
}
//对象在反序列化的时候调用
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
try {
// 读取对象的字段数据
ObjectInputStream.GetField readFields = ois.readFields();
// 从读取的字段数据中获取"password"字段的值,如果没有则返回空字符串
Object object = readFields.get("phone", "");
Object name = readFields.get("name", "");
Object age = readFields.get("age", 1);
// 打印需要解密的字符串
System.out.println("要解密的字符串:" + object.toString());
// 模拟解密过程,将"pass"作为解密后的密钥
this.phone = "pass";//模拟解密,需要获得本地的密钥
this.name = name.toString();
this.age = Integer.parseInt(age.toString());
} catch (IOException e) {
// 捕获并处理输入输出异常
e.printStackTrace();
} catch (ClassNotFoundException e) {
// 捕获并处理类不存在异常
e.printStackTrace();
}
}
/ 参考文章 /
https://juejin.cn/post/6973664796709404708
https://www.cnblogs.com/javazhiyin/p/11841374.html
原文始发于微信公众号(安全随心录):第十八课-系统学习代码审计:Java反序列化基础-原生方式的序列化和反序列化简单使用
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论