Java入门的简单基础,包括序列化反序列化、反射、类的动态加载,动态代理暂时还没用到,后面用到再加上。
序列化和反序列化
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
用处
- 想把内存中的对象保存到一个文件中或者数据库中时候;
- 想用套接字在网络上传送对象的时候;
- 想通过RMI传输对象的时候
实现
只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
通过 ObjectOutStream 包装 FileOutStream或者ByteArrayInputStream
同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象
ObjectOutputStream代表对象输出流:
- 它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流:
- 它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
例子
Person.java
|
|
SerializeTest.java
|
|
Unserialize.java
|
|
其他需要注意的点
- 静态成员变量是不能被序列化
- transient 标识的对象成员变量不参与序列化
重写writeObject和readObject方法
使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
example:
|
|
安全问题的产生
只要服务端反序列化数据,客户端传递的类的readObject中的代码就会自动执行,给予攻击者在服务器上运行代码的能力。
可能的形式
- 入口类的readObject直接调用危险方法。
- 入口类参数中包含可控类,该类有危险方法,readObject时调用。
- 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用。
- 构造函数/静态代码块等在类加载时隐式执行
利用
- 入口类
- 重写readObject
- readObject调用常见函数
- 参数类型宽泛
- 最好jdk自带
- 调用链 gadget chain
- 相同名称,相同类型
- 执行类
URLDNS
最简单的链,检测反序列化位点
|
|
然而put的时候直接调用了hashCode,需要通过反射改变已有对象的属性。
注意,java有dns缓存,所以同一个url,只会dns解析一次,后面都会直接在缓存里面找
拓展
Serializable 接口
一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。
这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。
由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。
使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
Externalizable 接口
它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。
因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。
对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
反射
Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
总的来时,反射赋予Java动态特性
反射的基本使用
获取Class对象
使用
Class.forName
静态方法1
Class class1 = Class.forName("Person");
使用类的
.class
静态属性1
Class class2 = Person.class;
使用实例对象的
getClass()
方法1 2
Person person = new Person(); Class class3 = person.getClass();
通过
ClassLoader
获取1 2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Class<?> c = systemClassLoader.loadClass("Person");
创造实例对象
- 通过Class的newInstance()方法
- 通过Constructor的newInstance()方法
|
|
方法一只能调用无参构造函数,一般不用
获取类的成员变量
|
|
修改成员变量
|
|
如何修改私有变量?
|
|
获取类的方法
|
|
调用类的方法
|
|
setAccessible同理
回到URLDNS
put之前修改hashCode属性不为-1;put之后,反序列化之前修改hashCode属性为-1
|
|
应用
- 定制需要的对象
- 通过invoke调用除了同名函数之外的函数
- 通过Class类创建对象,引入不能序列化的类
类的动态加载
类的加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载这7个阶段.其中其中验证、准备、解析3个部分统称为连接
- 加载:查找并加载类的二进制数据
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:类访问方法区内的数据结构的接口, 对象是Heap区的数据。
- 卸载
类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。 实现这个动作的代码模块称为“类加载器”。
类加载的三种方式
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
.class
和getClass()
使用之前类均已加载完毕
类加载机制
类的双亲委派机制
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
启动类加载器(Bootstrap ClassLoader)
这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
扩展类加载器(Extension ClassLoader)
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。
它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
举例如下:
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
类加载与代码执行
JVM初始化加载
初始化:静态代码块
实例化:构造代码块、无参构造函数
动态加载
Class.forname:初始化/不初始化皆可
ClassLoader.loadClass:不进行初始化
加载任意类
加载自己的恶意类,拓展攻击面
分析
|
|
|
|
利用
URLClassLoader任意类加载:file/http/jar
1 2 3 4 5
// URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///C:\\tmp\\")}); // URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:8888/")}); URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///C:\\tmp\\Hello.jar!/")}); Class<?> c1 = urlClassLoader.loadClass("Hello"); c1.newInstance();
ClassLoader.defineClass 字节码加载任意类
1 2 3 4
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("C:\\tmp\\Hello.class")); defineClassMethod.invoke(systemClassLoader,"Hello",code,0,code.length);
maven踩坑
- 中文官网下载jdk的链接有问题,需要去英文官网才正常
- 只有使用idea自带的maven才可以download source,否则会报错:
Sources not found for: commons-collections:commons-collections:3.2.1
参考
(2条消息) java序列化与反序列化全讲解_mocas_wang的博客-CSDN博客_java序列化讲解