长亭百川云 - 文章详情

[下班充电计划]java安全漫游学习笔记

Security丨Art

57

2024-07-13

java反序列化笔记

•获取类的⽅法: forName

•实例化类对象的⽅法: newInstance

•获取函数的⽅法: getMethod

•执⾏函数的⽅法: invoke

           

•obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过            
obj.getClass() 来获取它的类

•Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接            
拿它的 class 属性即可。这个⽅法其实不属于反射。

•Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取

           

⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。            其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,            
但在当前构造函数内容的前⾯。            
所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“。

           

我们经常在一些源码里看到,类名的部分包含 $ 符号,比如fastjson在 checkAutoType 时候就会            
先将 $ 替换成 . :https://github.com/alibaba/fastjson/blob/fcc9c2a/src/main/java/com/alibaba/fa            stjson/parser/ParserConfig.java#L1038。 $ 的作用是查找内部类

           

我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是            
method.invoke([1], [2], [3], [4]...) 。

           

方法 getConstructor 。和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。获取到构造函数后,我们使用 newInstance 来执行

           

另一种执行命令的方式ProcessBuilder

           

对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价            
的(也就不能重载):

public void hello(String[] names) {}            public void hello(String...names) {}

•getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法

•getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私            
有的方法,但从父类里继承来的就不包含了

•setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用            
setAccessible 修改它的作用域,否则仍然不能调用。

           

⼀个RMI Server分为三部分:

1.⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的 hello()

2.⼀个实现了此接⼝的类

3.⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server

           

⾸先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据            
流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=Hello的对象,这个对应            
数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址            
在 192.168.135.142:33769 ,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程            
⽅法调⽤,也就是 hello() 。

           

RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name            
到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI            
Server;最后,远程⽅法实际上在RMI Server上调⽤

           

Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、            
bind、unbind等方法。

           

只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,之前曾经有一个工具            https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。

           

在序列化Java类的时候用到了一个类,叫 ObjectOutputStream 。这个类内部有一个方法            
annotateClass , ObjectOutputStream 的子类有需要向序列化后的数据里放任何内容,都可以重写            
这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。

           

PHP的序列化是开发者不能参与的,开发者调用 serialize 函数后,序列化的数据就已经完成了,你得            
到的是一个完整的对象,你并不能在序列化数据流里新增某一个内容,你如果想插入新的内容,只有将            
其保存在一个属性中。也就是说PHP的序列化、反序列化是一个纯内部的过程,而其 sleep 、wakeup 魔术方法的目的就是在序列化、反序列化的前后执行一些操作。其实大部分PHP反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以控制对象的属性,进而在后续的代码中进行危险操作

 Extends可以理解为全盘继承了父类的功能。implements可以理解为为这个类附加一些额外的功能;interface定义一些方法,并没有实现,需要implements来实现才可用。extend可以继承一个接口,但仍是一个接口,也需要implements之后才可用。对于class而言,Extends用于(单)继承一个类(class),而implements用于实现一个接口(interface)。

序列化反序列化示例Person类需加main函数来看输出结果                public static void main(String[] args) throws Exception{                    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.txt"));                    Person p = new Person("312",3);                    out.writeObject(p);                    ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.txt"));                    in.readObject();                }

URLDNS

调试URLDNS过程按p神介绍断点在putvul中第一次计算hash为加载instrument.dll(应该是idea用于debug)

             

可以断点在ht.put(u, url);可以看到完整调用链

p神的介绍因为是入门比较简单,比较好玩的是su18改版的urldns,用来判断是否存在gadget需要的类

https://mp.weixin.qq.com/s/KncxkSIZ7HVXZ0iNAX8xPA

https://su18.org/post/gadgetor/

https://github.com/kezibei/Urldns

CC1

总体介绍和问题解析

CC链整体可以看这个

https://xz.aliyun.com/t/10357

https://www.cnblogs.com/litlife/p/12571787.html#transformer

总的来说就是AnnotationInvocationHandler反序列化过程中通过memberValues.entrySet()调用了,这里的memberValues是proxymap,mapProxy.entrySet(),任意proxy的方法调用都会调用proxy代理的invoke方法,这里的proxy设置为AnnotationInvocationHandler,所以调用AnnotationInvocationHandler的invoke方法,他的memberValues是lazymap,调用lazymap的get方法,如果key不存在就调用transfer方法,开启调用ChainedTransformer数组利用链

ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> this.memberValues.entrySet() = mapProxy.entrySet() -> AnnotationInvocationHandler.invoke() -> this.memberValues.get(xx) = LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> InvokerTransfomer.transform() -> RCE

cc1不能用的原因

MapstreamVals = (Map)fields.get("memberValues", null);                               强制类型转化吧我们的memberValues转换为Map,但实际调试可以发现此时的memberValues是proxy代理对象,所以会出错导致streamVals为null,我们设置的proxy没能成功传入反序列化过程,到了下面取null的entrySet自然取不到值

               

p牛是用transformedMap复现的,所以他说的高版本不能用的原因是他理解的transformedmap的原因,不是工具中使用lazymap在高版本不能用的原因

ysoserial在构造Transformers时,为什么要用那么多反射?

因为Runtime类没有继承Serializable接口,所以不能被序列化,在序列化时会出错,使用反射可以解决这个问题

反射代码解析

•在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数                
是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表。https://www.cnblogs.com/zpchcbd/p/14726562.html

p神的文章中设置如下参数                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),                               第二个参数是因为 getMethod 方法的第二个参数类型是 Class[],如果不需要传递任何参数,可以使用一个空的类对象数组。                               其实不传输一个空参也可以,默认置空                new InvokerTransformer("getMethod",new Class[]{String.class},new Object[]{"getRuntime"}),

•ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个                
对象,并在transform⽅法将这个对象再返回

•TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可                
以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map.其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。                
我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。

•Transformer是⼀个接⼝,它只有⼀个待实现的⽅法,TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调                
函数“,这个回调的参数是原始对象。

•ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串                
在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊。按顺序调用 Transformer 数组 this.iTransformers 中所有 Transformer 对象的 transform 方法,并且每次调用的结果传递给下一个 Transformer#transform() 作为参数

public Constructor<T>                       getDeclaredConstructor(Class[] parameterType)                       throws NoSuchMethodException, SecurityException

怎么触发回调呢?就是向Map中放⼊⼀个新的元素

动态代理

https://www.cnblogs.com/yy3b2007com/p/9065303.html

http://1.15.187.227/index.php/archives/457/

•InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法

•AnnotationInvocationHandler 判断第一个参数必须是注释类

•JAVA中间件在对序列化的AnnotationInvocationHandler类的对象数据进行反序列化时,会调用其readObject方法并触发漏洞

•因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,不能直接使用new来实例化。使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了

•sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是                
Annotation的子类,且其中必须含有至少一个方法,假设方法名是X

•被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素。所以,这也解释了为什么我前面用到 Retention.class ,因为Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素

lazymap

•lazymap是懒加载,在get找不到值的时候,它会调用factory.transform 方法去获取一个值。

ConstantTransformer(1)

最后会增加一个ConstantTransformer(1),隐蔽了启动进程的日志特征:

CC6

CC6

GPT侠时间

`org.apache.commons.collections.keyvalue.TiedMapEntry` 是 Apache Commons Collections 库中的一个类,它实现了 `java.util.Map.Entry` 接口。它的作用是将一个已存在的键值对(键和值)绑定到指定的 `Map` 实例。                           具体来说,`TiedMapEntry` 类提供了以下功能:                           1. 将键值对绑定到 `Map` 实例:通过构造函数,你可以将一个已存在的键值对绑定到 `TiedMapEntry` 对象,并指定要绑定的 `Map` 实例。这样,`TiedMapEntry` 对象就代表了该键值对在指定的 `Map` 中的绑定关系。                           2. 实现 `Map.Entry` 接口:`TiedMapEntry` 类实现了 `Map.Entry` 接口,它提供了访问和操作键值对的方法。你可以使用 `TiedMapEntry` 对象来获取键和值,并对其进行读取或修改。                           3. 与绑定的 `Map` 实例保持同步:`TiedMapEntry` 对象与绑定的 `Map` 实例保持同步,这意味着对 `TiedMapEntry` 对象的操作实际上会直接影响到绑定的 `Map` 实例中对应的键值对。反之亦然,对绑定的 `Map` 实例中对应的键值对的修改也会反映在 `TiedMapEntry` 对象上。                           通过使用 `TiedMapEntry`,你可以方便地将现有的键值对绑定到指定的 `Map` 实例,并使用 `TiedMapEntry` 对象对键值对进行访问和操作。这对于在某些场景下需要绑定和同步键值对的应用程序非常有用。

直接看cc6不如先看cc5,cc6的链子很简单,问题在于要remove我们的键值,p神给的解释是

问题出在第一遍调用put时候触发反序列化链时候lazymap#get方法没有找到key去触发transform后把键值put到map中,在反序列化过程中再次触发get方法直接再map中找到键值了,就不会触发后续的transform方法了

工具中的cc6通过反射方式将entry赋值给HashSet实例中的一个Node的key属性

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");                           HashSet map = new HashSet(1);              map.add("foo");//添加一个键                           Field f = null;              try {                  f = HashSet.class.getDeclaredField("map");//获取map属性              } catch (NoSuchFieldException e) {                  f = HashSet.class.getDeclaredField("backingMap");              }                           Reflections.setAccessible(f);              HashMap innimpl = (HashMap) f.get(map);//获取map实例的map属性。【也就是"foo"->】键值对                           Field f2 = null;              try {                  f2 = HashMap.class.getDeclaredField("table");              } catch (NoSuchFieldException e) {                  f2 = HashMap.class.getDeclaredField("elementData");              }                           Reflections.setAccessible(f2);              Object[] array = (Object[]) f2.get(innimpl);//获取map属性的table属性,里面包含很多Node                           Object node = array[0];              if(node == null){                  node = array[1];              }                           Field keyField = null;              try{                  keyField = node.getClass().getDeclaredField("key");              }catch(Exception e){                  keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");              }                           Reflections.setAccessible(keyField);              keyField.set(node, entry);//将其中一个Node的key属性改为entry                           return map;

java字节码

—所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在我们的探讨范围内

URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释              
URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程。

正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这              
些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

•URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻              
找.class文件

•URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻              
找.class文件

•URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

直接偷懒使用p牛的字节码会报错

Hello : Unsupported major.minor version 52.0

原因是jdk版本不同,因为调试cc1等低版本链子以及7u21的原生链子,所以我的jdk版本搞得很低是oracle jdk7u21,渣性能虚拟机懒得在idea里来回切jdk

需要自己写个类编译好然后读取并编码

package org.test.ser;                                                                               import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException;              import com.sun.org.apache.xml.internal.security.utils.Base64;                           import java.io.*;              import java.lang.reflect.InvocationTargetException;              import java.lang.reflect.Method;                           public class HelloDefineClass {                  public static void main(String[] args) throws NoSuchMethodException, Base64DecodingException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException {                      File file = new File("C:/Users/Administrator/IdeaProjects/test1/target/classes/org/test/ser/Hello.class");                      InputStream in = new FileInputStream(file);                      byte[] content = null;                      content = new byte[(int)file.length()];                      in.read(content);                      System.out.println(Base64.encode(content));                      Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);                      defineClass.setAccessible(true);                      //byte[] code = Base64.decode(content);                      byte[] code = content;                      Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(),"org.test.ser.Hello",code,0,code.length);                      hello.newInstance();                               }              }

注意根据class文件的全限定路径名需要根据自己写的包名修改,不然会报错

我的目录结构如下

下面的TemplatesImpl使用的工具类的setFieldValue,一开始以为是某个lib库中的方法,后来发现是spring的工具类,我这里感觉没有必要起spring,就把源码自己写在一个类中然后去引用

package org.test.ser;                           import java.lang.reflect.*;                           /**              * 反射的 Utils 函数集合              * 提供访问私有变量, 获取泛型类型 Class, 提取集合中元素属性等 Utils 函数              */                           public class ClassUtils {                                            /**                   * 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter                   *                   * @param object                   * @param fieldName                   * @return                   */                  public static Object getFieldValue(Object object, String fieldName) {                      Field field = getDeclaredField(object, fieldName);                                   if (field == null) {                          throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");                      }                                   makeAccessible(field);                                   Object result = null;                                   try {                          result = field.get(object);                      } catch (IllegalAccessException e) {                          //log.error("getFieldValue:", e);                      }                                   return result;                  }                               /**                   * 直接设置对象属性值, 忽略 private/protected 修饰符, 也不经过 setter                   *                   * @param object                   * @param fieldName                   * @param value                   */                  public static void setFieldValue(Object object, String fieldName, Object value) {                      Field field = getDeclaredField(object, fieldName);                                   if (field == null) {                          throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");                      }                                   makeAccessible(field);                                   try {                          field.set(object, value);                      } catch (IllegalAccessException e) {                          //log.error("setFieldValue:", e);                      }                  }                                            /**                   * 通过反射, 获得定义 Class 时声明的父类的泛型参数的类型                   * 如: public EmployeeDao extends BaseDao       <Employee< span>       </Employee<>, String>                     *                     * @param clazz                     * @param index                     * @return                     */                    @SuppressWarnings("unchecked")                    public static Class getSuperClassGenricType(Class clazz, int index) {                        Type genType = clazz.getGenericSuperclass();                                       if (!(genType instanceof ParameterizedType)) {                            return Object.class;                        }                                       Type[] params = ((ParameterizedType) genType).getActualTypeArguments();                                       if (index >= params.length || index < 0) {                            return Object.class;                        }                                       if (!(params[index] instanceof Class)) {                            return Object.class;                        }                                       return (Class) params[index];                    }                                   /**                     * 通过反射, 获得 Class 定义中声明的父类的泛型参数类型                     * 如: public EmployeeDao extends BaseDao        <Employee< span>        </Employee<>, String>                       *                       * @param                                         * @param clazz                       * @return                       */                      @SuppressWarnings("unchecked")                      public static <T> Class<T> getSuperGenericType(Class clazz) {                          return getSuperClassGenricType(clazz, 0);                      }                                       /**                       * 循环向上转型, 获取对象的 DeclaredMethod                       *                       * @param object                       * @param methodName                       * @param parameterTypes                       * @return                       */                      public static Method getDeclaredMethod(Object object, String methodName, Class[] parameterTypes) {                                           for (Class superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {                              try {                                  return superClass.getDeclaredMethod(methodName, parameterTypes);                              } catch (NoSuchMethodException e) {                                  //Method 不在当前类定义, 继续向上转型                              }                          }                                           return null;                      }                                       /**                       * 使 filed 变为可访问                       *                       * @param field                       */                      public static void makeAccessible(Field field) {                          if (!Modifier.isPublic(field.getModifiers())) {                              field.setAccessible(true);                          }                      }                                       /**                       * 循环向上转型, 获取对象的 DeclaredField                       *                       * @param object                       * @param filedName                       * @return                       */                      public static Field getDeclaredField(Object object, String filedName) {                                           for (Class superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {                              try {                                  return superClass.getDeclaredField(filedName);                              } catch (NoSuchFieldException e) {                                  //Field 不在当前类定义, 继续向上转型                              }                          }                          return null;                      }                                       /**                       * 直接调用对象方法, 而忽略修饰符(private, protected)                       *                       * @param object                       * @param methodName                       * @param parameterTypes                       * @param parameters                       * @return                       * @throws InvocationTargetException                       * @throws IllegalArgumentException                       */                      public static Object invokeMethod(Object object, String methodName, Class[] parameterTypes,                                                        Object[] parameters) throws InvocationTargetException {                                           Method method = getDeclaredMethod(object, methodName, parameterTypes);                                           if (method == null) {                              throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]");                          }                                           method.setAccessible(true);                                           try {                              return method.invoke(object, parameters);                          } catch (IllegalAccessException e) {                              //log.error("invokeMethod:", e);                          }                                           return null;                      }                  }

https://cloud.tencent.com/developer/article/1536342

原始代码在这里,Spring用Slf4j记录日志,我这里调试没什么用,又省的拉依赖回来,我就直接全注释掉了,然后在源码中import

import static org.test.ser.ClassUtils.setFieldValue;

调试时候建议在这里按照p牛说的链下断点

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->              TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()              -> TransletClassLoader#defineClass()

在obj.newTransformer();下断点后请直接到这里,不然前面会有很折磨的idea debugger调试器的加载过程

p神关于bcel的文章中有一个链接,把fastjson和bcel都讲清楚了

https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html

还有对应公众号,非常值得一看

CC2

在commons-collections中找Gadget的过 程,实际上可以简化为,找⼀条从 Serializable#readObject() ⽅法到 Transformer#transform() ⽅法的调⽤链

             

CommonsCollections2实际就是⼀条从 PriorityQueue 到 TransformingComparator 的利用链

传进去的是队列,然后调用Transforming.comparator的compare方法调用传入的chaintransformer的transformer方法

链子比较简单

Shiro550

https://www.jianshu.com/p/f87b6301d668              idea默认使用unicode编码,tomcat用的是utf-8,需要改下idea的配置文件,调试时候日志才不会出现乱码

[L是一个JVM的标记,说明实际上这是一个数组

shiro在tomcat下不能反序列化有数组的payload的原因是调用了ClassUtils的forName,在调用时使用的是URLClassLoader,会去把[Lorg.apache.commons.collections.Transformer当成path去加载类,有了[L,肯定找不到

cc版本shiro和payload对应cc版本要一致,不然会导致static final long serialVersionUID = -4803604734341277543L;]:报错

断点打在shiro-core中

可以看到调用hashmap的readobject

接着调用对key做hash到调用key的hashcode,到调用tidemap的hashcode

TidemapEntry的key是TemplatesImpl对象,他的_byte[]是我们传入的恶意类的byte,他的_class是已经从_byte还原的class

             

他的value是我们传入的lazymap的get(TemplatesImpl对象)

因为我们的lazy被transform修饰回调,也就是InvokerTransformer

所以会调用InvokerTransformer的transform方法

从而调用TemplatesImpl的newTransformer方法

进而调用getTransletInstance方法,实例化_class中的内容,也就是把我们的类实例化

在实例化过程中会调用初始化中的代码

执行恶意命令

CB1

PropertyUtils.getProperty 这个方法会自动去调用一个JavaBean的getter方法

调用TemplatesImpl利用链

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->              TemplatesImpl#getTransletInstance() ->              TemplatesImpl#defineTransletClasses() ->              TransletClassLoader#defineClass()

shiro默认带cb依赖

cb 在 BeanComparator 类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator,对cc有依赖

使用java.lang.String的CaseInsensitiveComparator显式传入Comparator ,使利用链不依赖cc,从而出现不需要额外的cc依赖,只要有shiro就能打

             

queue添加的必须是字符串String,不然会报错

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2