首页 > 其他分享 >FastJson反序列化1-FastJson基础使用及反序列化流程分析

FastJson反序列化1-FastJson基础使用及反序列化流程分析

时间:2024-03-17 22:34:35浏览次数:12  
标签:FastJson 解析 对象 流程 JSON 序列化 方法 加载

1、FastJson简介及使用

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

1.1 序列化Java Bean;

假设现在程序中有一个类User,基本信息如下(省略构造方法及get set方法):

package org.example.bean;

public class User {
    private String name;
    private Integer age;
}

使用fastjson将其转换为Json字符串:

@Test
    public void toJson(){
        User user = new User("xiaoming", 22);
        System.out.println(JSON.toJSONString(user));
        //{"age":22,"name":"xiaoming"}
    }
1.2 反序列化字符串

使用fastjson将json字符串反序列化为JavaBean:

@Test
public void toObject(){
    String s1="{\"age\":99,\"name\":\"xiaohong\"}";
    Object parse = JSON.parse(s1);
    System.out.println(parse+",类型为:"+parse.getClass());
    //{"name":"xiaohong","age":99},类型为:class com.alibaba.fastjson.JSONObject
}

2、反序列化漏洞

fastjson在1.2.24版本被曝出反序列化漏洞,原因是如果在JSON字符串中指定@type则可以根据字符串反序列化出任意类的对象,并且反序列化过程中会调用set(),在将此对象转换为JSON对象时,还会调用该对象的get()方法。

2.1 反序列化流程

2.1.1 未指定@type

断点位置:

这个函数调用了另一个重载形式的 parse() 方法,表示使用 Fastjson 默认的解析特性。

简单判断json数据是否为空。

创建了一个 DefaultJSONParser 对象,用于解析 JSON 字符串。ParserConfig 是 Fastjson 中用于配置 JSON 解析器的类,getGlobalInstance() 方法获取了一个默认的全局配置对象。

调用parse()方法后,返回的 value值就是解析得到的 Java 对象,跟进。

来到DefaultJSONParser类中,该方法是一个重载的 parse() 方法,用于解析 JSON 字符串并返回对应的 Java 对象。

来到该方法,根据 JSON 字符串的当前标记类型进行解析,并返回对应的 Java 对象。

获取当前 JSON 解析器的 lexer 对象,它是 JSONLexer 类的实例,负责解析 JSON 字符串。

来到switch判断,该方法中会根据当前 lexer 的标记类型进行不同的解析,将 JSON 字符串解析为相应的 Java 对象,根据 JSON 中的不同数据类型返回不同类型的 Java 对象。例如,set、TREE_SET、STRING、INT,不同的类型的解析方式略有不同。

创建一个 JSONObject 对象,并根据是否启用了 OrderedField 特性来决定其内部的键值对存储方式(键值对或HASH表)。

这个方法返回解析出的 JSONObject 对象

第一个if:

​ 检查当前的JSON解析器中的标记是否为空,为空则将解析器移动到下一个标记,并且返回null

第二个if:

​ 检查当前的标记是否为右大括号},表示JSON对象的结束,

第三个if:

​ 检查当前的标记是否为左大括号或者逗号,否则表示JSON语法错误。

获取上下文解析对象。

来到try catch中,大致意思如下:

  1. 在一个无限循环中,首先调用 lexer.skipWhitespace() 跳过 JSON 字符串中的空白字符,然后检查当前字符,根据不同的情况进行不同的处理。
  2. 根据当前字符的类型,进行相应的处理:
    • 如果是双引号("),表示是字符串类型的键值对,使用 lexer.scanSymbol 方法解析出键名,并检查是否为冒号(:),如果不是冒号则抛出异常。如果是冒号,则继续解析值部分。
    • 如果是右大括号(}),表示 JSON 对象的结束,返回解析得到的 JSONObject 对象。
    • 如果是单引号('),根据是否启用了 AllowSingleQuotes 特性,解析出键名,并检查是否为冒号,类似于双引号的处理。
    • 如果是数字(0-9),表示数字类型的键名,解析数字,并检查是否为冒号,类似于双引号的处理。
    • 如果是左中括号([),表示数组类型的值,调用 parseArray 方法解析数组,并将结果存储到 JSONObject 中。
    • 如果是左大括号({),表示嵌套的 JSON 对象类型的值,递归调用 parseObject 方法解析嵌套的 JSON 对象,并将结果存储到 JSONObject 中。
    • 如果是其他字符,根据是否启用了 AllowUnQuotedFieldNames 特性,解析出键名,并检查是否为冒号,类似于双引号的处理。
  3. 在解析完成后,跳过可能存在的逗号,并检查当前字符是否为右大括号,如果是则表示 JSON 对象解析结束,返回 JSONObject 对象。如果不是,则继续循环解析下一个键值对。
  4. 在 finally 块中,恢复解析上下文,确保解析过程的正确性。

会完成对键值对的处理。解析完成之后返回JSON对象。

2.1.2 指定@type

代码如下:

@Test
public void toObjectNoSafe(){
    String s1="{\"@type\":\"org.example.bean.User\",\"name\":\"hack\",\"age\":0}";
    Object o = JSON.parse(s1);
    System.out.println(o+",类型为:"+o.getClass());
    //org.example.bean.User@282ba1e,类型为:class org.example.bean.User

}

因为调试过程只有进入paseObject方法中的try语句中才有所变化,所以前面略过了。从下面的位置开始:

这个分支就是为了处理键为@type的JSON字符串,需要使用类加载动态创建对象;

具体来说,这段代码做了以下几件事情:

  • 判断键名是否为特殊键 @type,并且确保没有禁用特殊键检测特性。如果是,则继续处理,否则跳过这个键值对。
  • 获取键名 $type 对应的类型名称,通过 lexer.scanSymbol 方法解析出类型名称,并检查类型名称是否有效。
  • 根据类型名称加载对应的类对象,通过 TypeUtils.loadClass 方法加载类型对应的类对象,这里使用了配置中默认的类加载器。
  • 如果加载的类对象为空,则表示无法找到对应的类,这种情况下,将类型名称作为值存储到 JSON 对象中,并继续解析下一个键值对。
  • 如果加载的类对象不为空,则表示成功找到了对应的类,这时继续处理键值对的值部分。
  • 如果解析器当前标记为右大括号(JSONToken.RBRACE),则表示当前键值对是对象的最后一个键值对,直接返回 null。
  • 创建实例对象,根据找到的类对象,尝试创建一个新的实例对象。如果找到的类对象是一个 JavaBeanDeserializer,就使用它来创建实例。
  • 如果实例对象仍然为 null,则根据类对象的类型,创建一个默认的实例对象,比如如果是 Cloneable 类型,则创建一个 HashMap 对象,如果是空 Map,则创建一个空的 Map。
  • 设置解析器的解析状态为 TypeNameRedirect,表示正在处理类型名称重定向。
  • 如果当前解析上下文不为空,并且字段名称不是整数类型,则弹出解析上下文。
  • 如果对象的键值对数量大于 0,则将对象转换为指定类型的新对象,并进行后续的解析。
  • 如果以上步骤都无法满足,则根据类对象获取对应的反序列化器,通过反序列化器进行解析,并返回解析结果。

使用默认的类加载器进行类加载,跟进loadClassf方法;

判断是否为空,进不去;

此处的mappings是一个TypeUtils类中的一个属性。ConcurrentHashMap集合,键为String,值为Class<?>。在静态代码块中设定了一些键值对,实际上的作用相当于一个缓存表,缓存了一些常用的类,如果发现需要加载的类是该集合中的类,则直接返回该class对象,进行后续操作。

但是这里我们@type指定的类名显然不在表中,所以继续跟进。

如果类名以 "[" 开头,表示这是一个数组类型的类名,需要特殊处理。它会递归调用 loadClass 方法加载数组元素的类对象,并使用 Array.newInstance 方法创建一个数组的实例对象,最后返回数组的类对象。

如果类名以 "L" 开头并且以 ";" 结尾,表示这是一个类的全限定名,需要去掉头尾的字符后进行加载。它会递归调用 loadClass 方法加载去掉头尾字符后的类名,并返回对应的类对象。

如果指定了类加载器,则尝试使用指定的类加载器加载类。如果加载成功,则将加载得到的类对象放入缓存并返回。

如果没有指定类加载器,但当前线程的上下文类加载器不为 null,则尝试使用上下文类加载器加载类。如果加载成功,则将加载得到的类对象放入缓存并返回。

进入类加载过程;

加载完成之后将这个类放入缓存表中,返回类对象;

继续来到paseObject(final Map object, Object fieldName)方法,

如果成功加载到了类对象 clazz,则继续判断下一个 JSON token 是否为逗号(,)。如果是逗号,表示当前键值对后面还有其他键值对,需要继续解析;如果是右大括号(}),则表示当前键值对是对象的最后一个键值对,需要执行后续的操作。

如果下一个 token 是右大括号,说明当前对象解析结束,会尝试根据加载到的类对象 clazz 创建一个实例对象 instance

设置解析器的解析状态为 TypeNameRedirect,表示正在处理类型名称重定向。

根据类对象 clazz 获取对应的反序列化器 deserializer,进入该方法查看反序列化器;

该反序列化器中有一个IdentityHashMap<Type, ObjectDeserializer>(),其中存放了各种类型的反序列化器;

而我们传入的类型没有对应的反序列化器;

调用 getDeserializer(Class<?> clazz, Type type) 方法,以给定的类型 type 为参数,尝试获取对应的反序列化器。

查看是否有反序列化器、type是否为空,这里都进不去;

根据类上标记的 JSONType 注解中的目标映射类,尝试获取对应的反序列化器。但是我们并没有标注。

针对泛型类型,尝试从配置中获取对应的反序列化器;进不去;

获取类名,查看是否在该集合(黑名单)中,如果在则抛出异常,不允许进行反序列化操作;而该集合中只有一个类:

查看是否在awt包下,显然不在,跳过。

如果没有发生 JDK8 相关的错误,则检查是否为jdk8中一些特殊的类型,显然不是,跳过;

这里也不是,跳过。

Map.Entry 类型设置特定的反序列化器。

在运行时获取类加载器。

通过 SPI 机制加载并注册 AutowiredObjectDeserializer 的实现类,以扩展反序列化器的功能。

再次尝试从集合中获取反序列化器,但还是没获取到。

如果类型为枚举类型、数组类型、Set、HashSet、Collection、List 、ArrayList、或其子类、Throwable 类型,为其设置相应的反序列化器,但是都不是,所以创建JavaBeanDeserializer;

这里做完之后,就返回反序列化器进行反序列化工作了,所以跟进createJavaBeanDeserializer方法;这里使用的ASM技术,但是代码、分支比较多,大概意思就是根据一系列条件判断是否可以使用 ASM 生成 JavaBean 反序列化器,如果可以,则使用 ASM 生成器,否则返回普通的反序列化器。主要的规则如下:

  • 如果类是一个接口,则禁用 ASM。
  • 使用 JavaBeanInfo.build 方法创建类的信息。
  • 如果启用了 ASM,并且字段数量超过 200,则禁用 ASM。
  • 如果类没有默认构造函数,并且不是一个接口,则禁用 ASM。

build方法主要负责根据传入的类和类型构建一个用于描述 JavaBean 结构的信息对象,方便后续的序列化和反序列化操作。跟进;

获取所有构造器、字段、方法,继续跟进到下面的for循环中;

这段代码用于处理类中的方法,主要用于识别并解析成员变量的 getter 和 setter 方法,(先获取所有的set方法,在获取所有的get方法)并获取它们的注解信息。

在循环中,对于每个方法执行以下步骤:

  • 获取方法的名称,并存储在 methodName 变量中。
  • 检查方法名称的长度是否小于 4,如果是,则跳过这个方法。
  • 检查方法是否为静态方法,如果是,则跳过。
  • 检查方法的返回类型是否为 void 或者和声明方法的类相同,如果不是,则跳过。
  • 检查方法的参数数量是否为 1,如果不是,则跳过。
  • 获取方法上的 JSONField 注解,如果没有,则尝试获取父类中的注解。
  • 解析注解中的信息,包括序列化和反序列化特性、属性名称、排序值等。
  • 如果方法名不以 "set" 开头,则跳过该方法。
  • 解析属性名称,根据命名规则或者 JSONField 注解中的配置来确定属性名称。
  • 获取属性字段,如果字段不存在且方法是以 "is" 开头且参数类型是 boolean,则尝试获取以 "is" 开头的字段。
  • 获取字段上的 JSONField 注解,解析注解中的信息。
  • 根据命名策略处理属性名称。
  • 将解析到的字段信息存储到列表中。

最终将满足条件的方法名保存。

将解析到的字段信息添加到字段列表 fieldList 中。FieldInfo 是一个自定义的类,用于存储字段的详细信息,包括属性名称、对应的方法、字段对象、所属类、类型、排序值、序列化特性、反序列化特性以及注解信息等。

检查了是否存在方法信息(即 method 是否为非空)。如果存在方法信息,则表示该字段是通过方法获取的。

如果方法只有一个参数,则将该参数的类型赋给 fieldClass,并将其泛型类型赋给 fieldType

否则将方法的返回类型赋给 fieldClass,并将其泛型返回类型赋给 fieldType。同时,将字段的声明类设置为该方法的声明类。

如果方法信息为 null,表示字段是通过字段本身获取的。此时,将字段的类型赋给 fieldClass,将字段的泛型类型赋给 fieldType。同时,将字段的声明类设置为该字段的声明类。

如果字段被标记为 final,则将 getOnly 设置为 true,表示该字段为只读字段。

  • 检查方法名是否以 "get" 开头,并且第四个字符是大写字母。
  • 如果方法有参数,则跳过该方法的处理。
  • 检查方法的返回类型是否是 Collection、Map、AtomicBoolean、AtomicInteger 或 AtomicLong 中的一种。如果是这些类型之一,则通常不将其解析为一个属性,因此会跳过该方法的处理。
  • 如果方法满足以上条件,则根据方法名推断属性名。如果存在 JSONField 注解,并且该注解允许反序列化(即 deserialize() 方法返回 true),则使用注解中指定的属性名;否则,使用方法名中 "get" 后的部分作为属性名。
  • 接着,检查字段列表中是否已经存在相同属性名的字段信息,如果存在,则跳过该方法的处理。
  • 最后,如果存在 propertyNamingStrategy,则将属性名进行翻译处理,然后将新的字段信息添加到字段列表中。

处理完方法、字段后创建JavaBeanInfo并且返回;

此beanInfo信息如下:

经过一系列判断,最终由asmFactory创建一个反序列化器。

但是如果是使用asmFactory创建的反序列化器我们无法继续调试,所以只能让asm开关关闭,在上面的分析过程中发现如果有字段是getOnly(只有get方法没有Set方法,并且返回类型为以下类型)则可以关闭asm,从而使用默认的JavaBeanDeserializer。

添加一个字段,JavaBean如下:

public class User {
    private String name;
    private Integer age;
    private List list;
}



跟进该方法,进入ClassLoder中的checkPackageAccess,检查当前 ClassLoader 对象所加载的类的包访问权限,确保代码的安全性。尤其关注了非公共代理类和包访问权限的问题。权限检查没有问题之后,再次跟进new JavaBeanDeserializer语句。

进入该方法,主要作用是初始化 JavaBeanDeserializer 对象的状态和字段反序列化器。跳过

之后一路跳过到反序列化方法,跟进。

一路跳过,来到该方法,这里会调用构造方法创建对象实例,感兴趣可以看一眼,但是这里先跳过了。

跳过到setValue方法,这里会调用set方法为对象的字段赋值。

跟到这里基本就可以了。

标签:FastJson,解析,对象,流程,JSON,序列化,方法,加载
From: https://www.cnblogs.com/K1na-blog/p/18079315

相关文章

  • 软件产品开发从0到1的整个流程
    软件产品开发从0到1的整个流程顺序如下:1.市场调研与分析:分析市场需求、行业环境、竞争对手和潜在用户以确定产品定位和功能需求,制定项目计划和里程碑。2.产品规划:进行系统的预研,确定整体技术方案和资源投入。详细描述项目的目标、范围、预算、时间表、风险评估等信息,提交给相......
  • 程序流程的控制 Abort Exit Halt RunError Continue Break Sleep
    Break退出当前循环体,包括for,while,repeat等循环体;Continue用于从For语句,while语句或repeat语句强行结束本次循环,并开始下一次循环;Exit跳出当前代码块,也就是当前函数,跳出后是要继续向下执行的(如果有后续代码),若该代码为主程序,则终止该程序,若是函数或过程,则立即退出过程或......
  • 2024年江西教师招聘报名流程(建议电脑)
    2024年江西教师招聘报名流程(建议电脑)......
  • 24湖北教师招聘|报名流程!超详细
    2024湖北中小学教师招聘考试报名明天开始!建议用电脑报名时间:2024年3月15日9:00-3月21日17:00注意⚠此次湖北教师招聘共招9257人!✔️网上报名时间:3月15日9:00至3月21日17:00✔️笔试开始时间:2024年4月20日报名上传证件照要求:150*200px,jpg格式,最大500k,白底照片⚠️湖北义务教......
  • Spring Bean的生命周期流程
    前言Java中的公共类称之为JavaBean,而Spring中的Bean指的是将对象的生命周期,交给SpringIoC容器来管理的对象。所以Spring中的Bean对象在使用时,无需通过new来创建对象,只需要通过DI(依赖注入),从Spring中取出要使用的对象即可。在Spring框架中单实例Bean的初始化......
  • Spring 5.x 源码之旅-59AOP事务的初始化流程一
    作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬学习必须往深处挖,挖的越深,基础越扎实!阶段1、深入多线程阶段2、深入多线程设计模式阶段3、深入juc源码解析阶段4、深入jdk其余源码解析......
  • Spring 5.x 源码之旅-59AOP事务的初始化流程二
    作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬学习必须往深处挖,挖的越深,基础越扎实!阶段1、深入多线程阶段2、深入多线程设计模式阶段3、深入juc源码解析阶段4、深入jdk其余源码解析......
  • nicetool--替代hutool和fastjson的工具库
    ​前言如果你被hutool坑过、被fastjson坑过,nicetool帮你解脱!如果你想用稳定、Spring原生的工具类,nicetool已帮你封装!nicetool不生产工具,只是JDK和Spring的封装侠!介绍nicetool:超好用的Java工具类:稳定、方便。最大程度利用SpringBoot原生工具。官网:https://www.yuque.com/kni......
  • 性能测试(4)流程
    性能测试流程性能需求分析的结论或目标 被测系统:了解架构以及环境测试内容:一般是常用功能明确性能测试指标:无明确需求指标:需要自己和团队一起分析,或者查找资料与类似系统的类比。以及对未来流量的预估,确定性能需求的指标明确需求指标: 根据性能指标进行测确定性能指标:......
  • PHP引用序列化漏洞
    介绍根据php引用的特性构造序列化漏洞示例<?phpclassjust4fun{var$enter;var$secret;}$hxdyjx=newjust4fun();$hxdyjx->enter=&$hxdyjx->secret;echoserialize($hxdyjx);构造<?phpclassjust4fun{var$enter;var$secret;}$hxdy......