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

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

其实如果看了后面这里就很明白了:
image.png
这里直接判断是不是set开头的方法。
继续下去,
image.png
名称处理,将set后的大写字符转换为小写字符,比如这里的setAge的A转换为小写
下面还有下划线等文本处理,如果没找到,就寻找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