Java反序列化FastJson篇之原理分析(一)

依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>

知识准备

如果正常情况下,FastJson是如何被使用的呢?

参数获取和参数解析

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.natro92;

import com.alibaba.fastjson.JSONObject;

public class FastJsonNormalTest {
public static void main(String[] args) {
String jsonDemo = "{\"name\":\"Boogipop\",\"age\":\"Hacker\"}";
JSONObject jsonObject = JSONObject.parseObject(jsonDemo);
System.out.println(jsonObject);
System.out.println("I am " + jsonObject.getString("name") + ", and I am a " + jsonObject.getString("age") + "!");
}
}

image.png

对象解析

将Json格式解析为对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.natro92;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class FastJsonNormalTest {

public static void main(String[] args) {
String jsonDemo = "{\"name\":\"Boogipop\",\"age\":\"Hacker\"}";
Hacker hacker = JSON.parseObject(jsonDemo, Hacker.class);
System.out.println(hacker.getName());
}
}

class Hacker {
private String name;
private String age;
public Hacker() {
System.out.println("Hacker constructor");
}

public Hacker(String name, String age) {
this.name = name;
this.age = age;
}

public String getName() {
System.out.println("Hacker detect getName: " + name);
return name;
}

public void setName(String name) {
System.out.println("Hacker detect setName: " + name);
this.name = name;
}

public String getAge() {
System.out.println("Hacker detect getAge: " + age);
return age;
}

public void setAge(String age) {
System.out.println("Hacker detect setAge: " + age);
this.age = age;
}
}

// Hacker constructor
// Hacker detect setName: Boogipop
// Hacker detect setAge: Hacker
// Hacker detect getName: Boogipop
// Boogipop

我们能注意,这里调用了get set方法

使用type关键字序列化任意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.natro92;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class FastJsonNormalTest {

public static void main(String[] args) {
// String jsonDemo = "{\"name\":\"Boogipop\",\"age\":\"Hacker\"}";
String jsonDemo = "{\"@type\":\"com.natro92.Hacker\",\"name\":\"Boogipop\",\"age\":\"Hacker\"}";
JSONObject jsonObject = JSON.parseObject(jsonDemo);
System.out.println(jsonObject);
}
}

class Hacker {
private String name;
private String age;
public Hacker() {
System.out.println("Hacker constructor");
}

public Hacker(String name, String age) {
this.name = name;
this.age = age;
}

public String getName() {
System.out.println("Hacker detect getName: " + name);
return name;
}

public void setName(String name) {
System.out.println("Hacker detect setName: " + name);
this.name = name;
}

public String getAge() {
System.out.println("Hacker detect getAge: " + age);
return age;
}

public void setAge(String age) {
System.out.println("Hacker detect setAge: " + age);
this.age = age;
}
}
// Hacker constructor
// Hacker detect setName: Boogipop
// Hacker detect setAge: Hacker
// Hacker detect getAge: Hacker
// Hacker detect getName: Boogipop
// {"name":"Boogipop","age":"Hacker"}

在实例化的过程调用了get、set方法来修改值。

流程分析

调试

JSON.parseObject(jsonDemo)断点。
image.png
进入
image.png
再进
image.png

image.png
这里的Parse.parse进去能发现,其中调用的parseObject方法,
image.png
里面对key判断是否是需要进行java反序列化或者是json反序列化,因为匹配到了关键字@type,所以进行java反序列化。
image.png
然后调用类加载loadClass
image.png
构建反序列化器,然后反序列化,进入getDeserializer
image.png
再进getDeserializer
image.png
这里提前内置了一些反序列化类,但是自己写类的肯定没有。
步过。
image.png
这里解析了JsonType注解。
image.png
继续过,能注意到这里有个黑名单,里面是线程,可能就会出现一些问题。
然后就是匹配是否是一些类,比如time
image.png
再往下,检测是否是枚举或者数组等这些数据类型。
image.png
这里如果都不是,那么就会默认创建一个JavaBean来处理:
image.png
进去。
image.png
asmEnable是用来动态创建动态加载。
image.png
然后判断一下获取什么参数之类的。如果满足就改成false
但是这里出了个问题,我这里会在这个getTypeParameters方法中长度不为零,导致asmEnable赋值false,无法继续下去。
因此可以更改格式,把Hacker类独立成一个java文件,然后重新调试这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.natro92;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class FastJsonNormalTest {

public static void main(String[] args) {
// String jsonDemo = "{\"name\":\"Boogipop\",\"age\":\"Hacker\"}";
String jsonDemo = "{\"@type\":\"com.natro92.Hacker\",\"name\":\"Boogipop\",\"age\":\"Hacker\"}";
JSONObject jsonObject = JSON.parseObject(jsonDemo);
System.out.println(jsonObject);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.natro92;

public class Hacker {
private String name;
private String age;
public Hacker() {
System.out.println("Hacker constructor");
}

public Hacker(String name, String age) {
this.name = name;
this.age = age;
}

public String getName() {
System.out.println("Hacker detect getName: " + name);
return name;
}

public void setName(String name) {
System.out.println("Hacker detect setName: " + name);
this.name = name;
}

public String getAge() {
System.out.println("Hacker detect getAge: " + age);
return age;
}

public void setAge(String age) {
System.out.println("Hacker detect setAge: " + age);
this.age = age;
}
}

这样就可以了:
image.png
继续,进入这个JavaBeanInfobuild方法。
image.png
我们可以看调试的这些变量,methods获取默认构造方法,declaredFields获取变量
image.png
默认方法不仅有getset这些还有一些Exception类,
然后构造器:
image.png
然后继续过
image.png
允许访问构造函数,下面这里比较重要:
image.png
遍历方法,获取你的方法名称字段,然后判断长度、判断是不是static、不是返回空这些都跳过。
image.png
这里我也懵逼了,看了视频我才明白。这里遍历三次,第一次遍历方法,第二次获取字段,第三次又遍历一次方法。
这里的意思是第一次找所有的setter,第二次要找所有的静态方法,第三次要找getter

setterset的长度不会是三个了,一般的返回值是void,这里就解释通了

其实如果看了后面这里就很明白了:
image.png
这里直接判断是不是set开头的方法。
继续下去,
image.png
名称处理,将set后的大写字符转换为小写字符,比如这里的setAgeA转换为小写
下面还有下划线等文本处理,如果没找到,就寻找is开头的方法名:
image.png
提高对多种命名的判断。
然后继续下去
image.png
创建FieldInfo,进入
image.png
image.png
统一名称,如果不一样统一。然后一直过下去。
image.png
这里没法进到else里面,一路过下去,都进不去。
就出add方法了。
这里没有static方法,就跳过
然后下去到遍历getter方法:
image.png
前面的逻辑都类似,长度、静态方法等
image.png
image.png
如果返回格式是这几种而且没有setter、只有getter,就会进入、调用add方法。
但是我们的测试demo都不满足。
然后就到下面返回处:
image.png
返回创建的构造函数。
然后出去,我们就能看到beanInfo找到所有的字段
image.png
过下去
image.png
如果是getOnly、不是Public类、不是成员类等,就会把asmEnable关掉,这里就是前面不会进入到asmEnable的原因,前面就有一个类判断。
我们继续往下可以发现,如果这里asmEnable是false,这里就会创建一个JavaBeanDeserializer
image.png
然后再build一次。
这里我们下到asmFactory.createJavaBeanDeserializer这里,用asm创建一个反序列化器。
这里的临时反序列化器无法进行调试。
因此我们需要满足如果是getOnly是true,这里就不会调用asmEnable
image.png

PS: 这里看了评论区其实有别的解法
关于绕过ASM动态生成类去调试的方法,直接关闭asm:
调试toJSONString:SerializeConfig.getGlobalInstance().setAsmEnable(false);
调试反序列化函数:ParserConfig.getGlobalInstance().setAsmEnable(false);

image.png
其中FieldInfo这个构造里面能改,需要让getParameterTypes不唯一
但是这里只有get方法能进来不行。
这里看到的解决办法是添加一个Map类型的参数,且只有get方法的属性,比如:

1
2
3
4
5
private Map map;
public Map getMap() {
System.out.println("Hacker detect getMap: " + map);
return map;
}

如果有set方法就不会存到get方法里面了。
重新断点,回到遍历setter、getter方法那里(JavabeanInfo类下的328行开始):
image.png
直接跳到遍历getter(490行):
进去方法到返回类型判断
image.png
返回的是Map类型可以进来
然后到最后add进来
image.png
进一下FieldInfo这里,找到我们需要的getOnly这里:
因为当前参数值是零,所以会进到下面的else。
image.png
getOnly就变成了true
然后就可以到ParserConfig的对asmEnable这里的判断。
image.png
进去
image.png
直接过,过到derializer接到返回值
image.png
我们看下里面:
image.png
这里得到了几个字段。
然后调用反序列化。
过出去,现在就得到了反序列化器。
image.png
然后进入deserialize反序列化方法。
一路进
image.png
往下:
image.png
遍历字段信息。一路下去
image.png
创建实例
image.png
如果是接口需要创建动态代理,如果不是调用构造函数。
image.png
赋值
image.png
一路过
image.png
在下面执行invoke
image.png
image.png
执行了set
那get在哪?get在Json.toJSON方法这里
image.png
也就是我们最开始到的位置(又回到最初的起点)
也就是将json转成对象调用setter,然后对象转成json又调用getter
image.png
我们直接跳转过来,果然没有执行。
一路进,然后进入getObjectWriter
image.png
往下来到buildBeanInfo
image.png
这里获取到fieldInfoList,获取到参数,到这里时已经有method了。
一路过回到JSON
image.png
断下FieldInfo的get方法
image.png
在这里invoke执行的get方法。
image.png