FastJson反序列化1-FastJson基础使用及反序列化流程分析
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中,大致意思如下:
- 在一个无限循环中,首先调用 lexer.skipWhitespace() 跳过 JSON 字符串中的空白字符,然后检查当前字符,根据不同的情况进行不同的处理。
- 根据当前字符的类型,进行相应的处理:
- 如果是双引号("),表示是字符串类型的键值对,使用 lexer.scanSymbol 方法解析出键名,并检查是否为冒号(:),如果不是冒号则抛出异常。如果是冒号,则继续解析值部分。
- 如果是右大括号(}),表示 JSON 对象的结束,返回解析得到的 JSONObject 对象。
- 如果是单引号('),根据是否启用了 AllowSingleQuotes 特性,解析出键名,并检查是否为冒号,类似于双引号的处理。
- 如果是数字(0-9),表示数字类型的键名,解析数字,并检查是否为冒号,类似于双引号的处理。
- 如果是左中括号([),表示数组类型的值,调用 parseArray 方法解析数组,并将结果存储到 JSONObject 中。
- 如果是左大括号({),表示嵌套的 JSON 对象类型的值,递归调用 parseObject 方法解析嵌套的 JSON 对象,并将结果存储到 JSONObject 中。
- 如果是其他字符,根据是否启用了 AllowUnQuotedFieldNames 特性,解析出键名,并检查是否为冒号,类似于双引号的处理。
- 在解析完成后,跳过可能存在的逗号,并检查当前字符是否为右大括号,如果是则表示 JSON 对象解析结束,返回 JSONObject 对象。如果不是,则继续循环解析下一个键值对。
- 在 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反序列化1-FastJson基础使用及反序列化流程分析的更多相关文章
- Hadoop基础-序列化与反序列化(实现Writable接口)
Hadoop基础-序列化与反序列化(实现Writable接口) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.序列化简介 1>.什么是序列化 序列化也称串行化,是将结构化 ...
- [java安全基础 01]SQL+反序列化
tomcat Servlet 什么是servlet Java Servlet是运行在 Web 服务器或应用服务器上的程序.它是作为来自Web浏览器或其他HTTP客户端的请求和HTTP服务器上的数据库或 ...
- 【Django drf】 序列化类常用字段类和字段参数 定制序列化字段的两种方式 关系表外键字段的反序列化保存 序列化类继承ModelSerializer 反序列化数据校验源码分析
目录 序列化类常用字段类和字段参数 常用字段类 常用字段参数 选项参数 通用参数 序列化类高级用法之source source填写类中字段 source填写模型类中方法 source支持跨表查询 定制 ...
- node.js基础模块http、网页分析工具cherrio实现爬虫
node.js基础模块http.网页分析工具cherrio实现爬虫 一.前言 说是爬虫初探,其实并没有用到爬虫相关第三方类库,主要用了node.js基础模块http.网页分析工具cherri ...
- List-LinkedList、set集合基础增强底层源码分析
List-LinkedList 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 继上一章继续讲解,上章内容: List-ArreyLlist集合基础增强底层源码分析:https:// ...
- List-ArrayList集合基础增强底层源码分析
List集合基础增强底层源码分析 作者:Stanley 罗昊 [转载请注明出处和署名,谢谢!] 集合分为三个系列,分别为:List.set.map List系列 特点:元素有序可重复 有序指的是元素的 ...
- Java04-Java语法基础(三)流程控制
Java04-Java语法基础(三)流程控制 一.数据类型的转换 1.自动转换:在赋值运算中,占字节数大的类型会自动向字节小的类型转换 double d1 = 3.14; int t1 = d1; 2 ...
- 关于Linux开源项目基础组件make编译流程
关于Linux开源项目基础组件make编译流程 非常多Linux开源项目都会用到编译出可运行文件的make.这个是有一套流程的. 首先,GNU构建系统:https://en.wikipedia. ...
- Java基础语法(5)-特殊流程控制语句
title: Java基础语法(5)-特殊流程控制语句 blog: CSDN data: Java学习路线及视频 1.嵌套循环结构 将一个循环放在另一个循环体内,就形成了嵌套循环.其中,for ,wh ...
- 【Hadoop离线基础总结】流量日志分析网站整体架构模块开发
目录 数据仓库设计 维度建模概述 维度建模的三种模式 本项目中数据仓库的设计 ETL开发 创建ODS层数据表 导入ODS层数据 生成ODS层明细宽表 统计分析开发 流量分析 受访分析 访客visit分 ...
随机推荐
- 从零开始手写 mybatis(一)MVP 版本
什么是 MyBatis ? MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射. MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集. MyB ...
- Zabbix 配置笔记
Zabbix Server 安装参考 https://www.cnblogs.com/clsn/p/7885990.html 安装脚本 #!/bin/bash #clsn #设置解析 注意:网络条件较 ...
- MyBatis Interceptor
MyBatis的拦截器可以用于在以下对象及方法中拦截修改: Executor (update, query, flushStatements, commit, rollback, getTransac ...
- java打印杨辉三角
package com.dylan.practice.interview; /** * 打印杨辉三角 * * 原理 * 1.每个数等于它上方两数之和 * 2.第n行的数字有n个 * * @author ...
- 【Android 逆向】【攻防世界】android2.0
这是一道纯算法还原题 1. apk安装到手机,提示输入flag,看来输入就是flag 2. jadx 打开apk查看 this.button.setOnClickListener(new View.O ...
- 学习go语言编程之函数
函数定义 函数的基本组成:关键字func,函数名,参数列表,返回值,函数体,返回语句. 示例如下: func Add(a int, b int) (ret int, err error) { if a ...
- SpringBoot使用令牌桶算法+拦截器+自定义注解+自定义异常实现简单的限流
令牌桶 在高并发的情况下,限流是后端常用的手段之一,可以对系统限流.接口限流.用户限流等,本文就使用令牌桶算法+拦截器+自定义注解+自定义异常实现限流的demo. 令牌桶思想 大小固定的令牌桶可自行以 ...
- 项目实战:C#上位机+arduino下位机+控制点亮LED灯
前言 当前比较流行的arduino开发,联动做一个Demo. 应用构架 上位机:C#上位机通过串口发送接收控制协议,来控制下位机: 下位机:arduino下位机主控,接受上位机串口协议控 ...
- MongoDB的安装及启动
下载地址 https://www.mongodb.com/try/download/community 安装步骤 自定义安装目录 配置环境 下面是你安装后的mongodb的目录 在电脑的环境变量Pat ...
- 安装MySql失败( Microsoft Visual C++ 2013 Runtime 64bit)
参考资料:下载之家 提示你缺少什么版本就安装什么版本.64位或者32位. 文件下载地址:下载之家 不知道有没有失效,如果失效的话大家直接去下载之家搜索下载.