【Java】【重要机制】详解序列化与反序列化
一、什么是序列化和反序列化
- 序列化:可以将对象转化成一个字节序列,便于存储。
- 反序列化:将序列化的字节序列还原
二、序列化和反序列化的优点
- 可以实现对象的 ” 持久性 ” , 所谓持久性就是指对象的生命周期不取决于程序。
- 利用序列化实现远程通信,在网络上传输字节序列。
三、序列化和反序列化的缺点
使用Java内置序列化功能的主要场景是为了在当前进程之外保存对象并在需要的时候重新获得对象。有以下缺点:
- 序列化会不必要地对外公开了对象的物理实现
- 序列化会容易使一个类对其最初的内部表示产生依赖
- 编写正确的反序列化代码有很大的挑战
- 序列化会增大了安全风险
- 序列化会增加了测试的难度
综上,序列化耦合了对象的逻辑信息和物理实现,使得开发者在面对领域需求之外需要额外关注很多专有的细节知识。在可能的情况下,使用其他替代方案将会减少工作量、减少bug、降低出现安全漏洞的风险。
因此,建议除非必须使用的第三方接口要求必须实现Serializable接口,否则应选用其他方式代替。
四、实现序列化和反序列化的方式
(1) 实现 Serializable 接口 ( 隐式序列化 )
通过实现 Serializable 接口,这种是隐式序列化 ( 不需要手动 ) ,这种是最简单的序列化方式,会自动序列化所有非 static 和 transient 关键字修饰的成员变量。注意点:
-
实现Serializable接口的可序列化类应该显式声明serialVersionUID
如果可序列化类未显式声明serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认serialVersionUID值,如“Java(TM) 对象序列化规范”中所述。但是,强烈建议所有可序列化类都显式声明serialVersionUID值,原因是计算默认的serialVersionUID对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的InvalidClassException 。因此,为保证serialVersionUI值跨不同java编译器实现的一致性,序列化类必须声明一个明确的serialVersionUID值。同时强烈建议使用private显式声明serialVersionUID,原因是这种声明仅应用于当前声明类,serialVersionUID作为继承成员没有用处。
-
序列化对象中的HashMap、HashSet或HashTable等集合禁止包含对象自身的引用
如果一个被序列化的对象中包含HashMap、HashSet或HashTable集合,则这些集合中不允许保存当前被序列化对象的直接或间接引用。因为,这些集合类型在反序列化的时候,会调用到当前序列化对象的hashCode方法,而此时(序列化对象还未完全加载)计算出的hashCode有可能不正确,从而导致对象放置位置错误,破坏反序列化的实例。
-
不要直接序列化指向系统资源的信息
当序列化结果中含有指向系统的资源时,这些信息很容易被篡改。当恶意用户篡改了指向系统的资源时,反序列化的对象会直接操作这些被攻击者指定的系统资源,导致任意文件读取或修改。因此,建议实现Serializable的类,其成员变量为File或FileDescriptor时,用transient修饰,避免这些对象被序列化。
-
不要序列化非静态的内部类
内部类是没有显式或隐式声明为静态的嵌套类。内部类(包括本地类和匿名类)的序列化很容易出错。
- 在使用非静态内部类时,实际上隐含着对外部类实例的非transient引用,在对内部类进行序列化时,会一起将外部类也序列化。
- 内部类的实现与synthetic属性有关,对synthetic关键字,不同的编译器的实现不同,会影响程序的兼容性。并且会跟默认的serialVersionID产生冲突。
- 内部类不能声明静态成员以外的运行时常量,所以不能使用serialPersistentFields机制来指定可以序列化的属性。
- 与外部实例关联的内部类没有无参构造方法(此内部类的构造方法隐式的接收外部实例作为前置参数)。内部类无法实现Externalizable接口,Externalizable接口要求实现对象通过writeExternal() 和readExternal() 方法手动保存和恢复其状态。
基于以上原因,禁止序列化非静态内部类。但是这些原因不适用于静态内部类,所以静态内部类可以进行序列化。
synthetic是什么?编译器通过生成一些在源代码中不存在的synthetic方法和类的方式,实现了对private级别的字段和类的访问,从而绕开了语言限制,这可以算是一种trick。在实际生产和应用中,基本不存在程序员需要考虑synthetic的地方。
PS: 在此提一个的常见的存在synthetic的案例。如果同时用到了Enum和switch,如先定义一个enum枚举,然后用switch遍历这个枚举,java编译器会偷偷生成一个synthetic的数组,数组内容是enum的实例。
-
序列化操作要防止敏感信息泄露
序列化操作是将一个对象转换为一个字节码或字符序列,该过程中,默认是没有进行安全防护的,数据都是明文的。当序列化结果中含有敏感信息时,序列化结果在磁盘上存储、跨信任域传递等操作都存在敏感信息泄露风险。所以在序列化操作中,应该避免序列化敏感信息, 如果敏感信息必须序列化,需要先对敏感信息进行加密或对序列化结果进行加密,跨信任边界传递含敏感信息的序列化结果时需要先签名后加密。
-
防止反序列化被利用来绕过构造方法中的安全操作
反序列化操作可以在不执行构造方法的情况下创建对象的实例,所以反序列化操作中的行为应该设计为与构造方法保持一致,这些行为包括:
- 对参数的校验;
- 安全管理器的检查;
- 对属性赋初始值,特别是transient修饰的属性反序列化操作默认不会赋值;
否则,攻击者就可能会通过反序列化操作构造出与预期不符合的对象实例。
-
不要直接将外部数据进行反序列化
反序列化操作是将一个二进制流或字符串反序列化为一个Java对象。当反序列化操作的数据是外部数据时,恶意用户可利用反序列化操作构造指定的对象、执行恶意代码、向应用程序中注入有害数据等。不安全反序列化操作可能导致任意代码执行、特权提升、任意文件访问、拒绝服务等攻击。实际应用中,通常采用三方件实现对json、xml、yaml格式的数据序列化和反序列化操作。常用的三方件包括:fastjson、jackson、XMLDecoder、XStream、SnakeYmal等。
具体代码实现:
编写需要序列化的类对象和序列化工具:
class Student implements Serializable {
private static String number;//学号
private transient String name;//姓名
private ArrayList<String> teachers;//老师
public Student() {
}
public Student(String number, String name, ArrayList<String> teachers) {
this.number = number;
this.name = name;
this.teachers = teachers;
}
public String getNumber() {
return number;
}
@Override
public String toString() {
return "Student{" +
"number='" + number + '\'' +
", name='" + name + '\'' +
", teachers=" + teachers +
'}';
}
public void setNumber(String number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ArrayList<String> getTeachers() {
return teachers;
}
public void setTeachers(ArrayList<String> teachers) {
this.teachers = teachers;
}
}
/*序列化工具类:将对象状态保存至文件中*/
class SerializeUtil {
/**
* 将对象序列化到指定文件中
* @param obj
* @param filename
* @throws IOException
*/
public static void mySerialize(Object obj,String filename) throws IOException {
OutputStream outputStream = new FileOutputStream(filename);//创建一个文件输出流对象
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);//序列化输出字节流,将输出对象outputStream传递给输出流
objectOutputStream.writeObject(obj);//写入
objectOutputStream.close();//关闭流
}
/**
* 从指定文件中反序列化对象
* @param filename
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
public static Object myDeSerialize(String filename) throws IOException, ClassNotFoundException {
InputStream inputStream = new FileInputStream(filename);//创建一个文件输入流
ObjectInputStream objectInputStream =new ObjectInputStream(inputStream);//反序列化输出字节流,将输入对象inputStream传递给输入流
Object object = objectInputStream.readObject();//读取
objectInputStream.close();
return object;
}
}
测试:
1)序列化:
public class SerializeTest {
public static void main(String[] args) {
ArrayList<String> teacherArrayList = new ArrayList<>();
teacherArrayList.add("李老师");
teacherArrayList.add("班班");
Student student1 = new Student("1001","WQS",teacherArrayList);
System.out.println("原始对象"+student1);
String fileName = "d://studnet.txt";
try {
//对象序列化
SerializeUtil.mySerialize(student1,fileName);
System.out.println("序列化原始对象完成!OK!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
2)反序列化:
public class SerializeTest {
public static void main(String[] args) {
String fileName = "d://studnet.txt"; // 改文件已经将对象的状态序列化进去了
try {
//对象的反序列化
Object obj=SerializeUtil.myDeSerialize(fileName);
if(obj instanceof Student){
Student studentNew= (Student) obj;
System.out.println("反序列化之后的对象:"+studentNew);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:反序列化之后的对象:Student{number='null', name='null', teachers=[李老师, 班班]}
说明:static或transient修改的变量不会被序列化。
(2)实现Externalizable接口。(显式序列化)
Externalizable 接口继承自 Serializable, 我们在实现该接口时,必须实现 writeExternal() 和readExternal() 方法,而且只能通过手动进行序列化,并且两个方法是自动调用的,因此,这个序列化过程是可控的,可以自己选择哪些部分序列化
public class Blip implements Externalizable{
private int i ;
private String s;
public Blip() {}
public Blip(String x, int a) {
System.out.println("Blip (String x, int a)");
s = x; i = a;
}
public String toString() { return s+i; }
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
System.out.println("Blip.writeExternal");
out.writeObject(s);
out.writeInt(i);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// TODO Auto-generated method stub
System.out.println("Blip.readExternal");
s = (String)in.readObject();
i = in.readInt();
}
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
System.out.println("Constructing objects");
Blip b = new Blip("A Stirng", 47);
System.out.println(b);
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("F://Demo//file1.txt"));
System.out.println("保存对象");
o.writeObject(b);
o.close();
//获得对象
System.out.println("获取对象");
ObjectInputStream in = new ObjectInputStream(new FileInputStream("F://Demo//file1.txt"));
System.out.println("Recovering b");
b = (Blip)in.readObject();
System.out.println(b);
}
}
(3)实现Serializable接口+添加writeObject()和readObject()方法(显+隐序列化)
如果想将方式一和方式二的优点都用到的话,可以采用方式三, 先实现 Serializable 接口,并且添加writeObject() 和 readObject() 方法。注意这里是添加,不是重写或者覆盖。但是添加的这两个方法必须有相应的格式。
- 方法必须要被 private 修饰 —–> 才能被调用
- 第一行调用默认的 defaultRead/WriteObject() —–> 隐式序列化非 static 和 transient
- 调用 read/writeObject() 将获得的值赋给相应的值 —–> 显式序列化
public class SerDemo implements Serializable{
public transient int age = 23;
public String name ;
public SerDemo(){
System.out.println("默认构造器。。。");
}
public SerDemo(String name) {
this.name = name;
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeInt(age);
}
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
stream.defaultReadObject();
age = stream.readInt();
}
public String toString() { return "年龄" + age + " " + name; }
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerDemo stu = new SerDemo("Ming");
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(stu);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
SerDemo stu1 = (SerDemo) in.readObject();
System.out.println(stu1);
}
}
(4)json序列化和反序列化(常常用于网络传输)
Json 序列化常用的三方件包括:fastjson、jackson、XMLDecoder、XStream、SnakeYmal等,将对象转化为 byte 数组或者将 json 串转化为对象。现在的大多数公司都将 json 作为服务器端返回的数据格式。
五、总结
(1)java代码中,对于实现了Serializable,Externalizable接口的class可以实现反序列化操作。反序列化操作是将一个二进制流或字符串反序列化为一个java对象.
(2)当反序列化的数据是不可信数据时,反序列化操作可以创建攻击者指定的类对象。另外java系统目前存在很多的三方件提供了反序列化功能,常见如:xml格式的反序列化XMLDecoder、XStream;json格式的反序列化fastjson、jackson等。一些常用的JSON框架都具有type功能,它可以很方便的将java的对象类型和json数据格式之间进行转换。但是这个功能存在严重漏洞,通过它攻击者可以传入任意class的类型,最终导致JSON框架的反序列化命令执行漏洞。这里尽量不要使用这个type功能,如果非要使用,一般情况JSON框架提供type的白名单机制,利用白名单指定到具体类名,可以有效防止漏洞触发。所以,对于json反序列化操作,必须禁止开启JSON框架的type功能,如果无法禁止type功能,必须使用白名单校验。不同的json框架,默认是否禁用type功能,不同的版本之间会存在差异,使用过程中要根据实际情况进行正确的设置。
(3)虽然序列化可以将对象的状态保存为一个字节序列,之后通过反序列化将字节序列又能重新构造出原来的对象,但是它并没有提供一种机制来保证序列化数据的安全性。因此,敏感数据序列化之后是潜在对外暴露的,可访问序列化数据的攻击者可以借此获取敏感信息并确定对象的实现细节。永远不应该被序列化的敏感信息包括:密钥、数字证书、以及那些在序列化时引用敏感数据的类,防止敏感数据被无意识的序列化导致敏感信息泄露。另外,声明了可序列化标识对象的所有字段在序列化时都会被输出为字节序列,能够解析这些字节序列的代码可以获取到这些数据的值,而不依赖于该字段在类中的可访问性。因此,若其中某些字段包含敏感信息,则会造成敏感信息泄露。防止未加密的敏感数据被序列的方法有:
- 使用transient定义敏感数据
- 使用serialPersistentFields定义非敏感数据
- 重新定义Serializable接口的writeObject()、writeReplace()、writeExternal()这些函数,不将包含敏感信息的字段写到序列化字节流中。
- 在在序列化与反序列化涉及的writeObject()和readObject()方法中使用安全管理器
- 点赞
- 收藏
- 关注作者
评论(0)