pom 依赖如下,使用的 solon 版本为 2.8.4

1
2
3
4
5
6
<parent>  
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>2.8.4</version>
<relativePath />
</parent>

请求的处理流程图可以参考官方文档
![[_resources/solon 框架请求处理流程分析/15abb79c9f150557722aa3a43977549f_MD5.jpeg]]
这里以一个 postController 举例来分析处理请求的过程

1
2
3
4
5
6
7
8
9
10
11
@Mapping("/api")  
@Post
public String api(Map map, Context ctx) throws Exception {
JSONObject jsonObject = new JSONObject(ctx.body());
if (map.size() != jsonObject.length()) {
User user = (User)deserialize((String)map.get("data"));
return user.getName();
} else {
return "success";
}
}

ActionExecuteHandlerDefault

这里用 burp 发送一个正常的 post 请求

1
2
3
4
5
6
7
POST /api HTTP/1.1
Host: 192.168.13.1:8888
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 9

a=b&map=1

入口从org.noear.solon.core.route.RouterHandler#handleMain 开始,直到调用 org.noear.solon.core.mvc.ActionDefault#executeDo 方法

1
2
3
4
5
protected Object executeDo(Context c, Object obj) throws Throwable {  
ActionExecuteHandler executeHandler = Solon.app().chainManager()
.getExecuteHandler(c, mWrap.getParamWraps().length);
return executeHandler.executeHandle(c, obj, mWrap);
}

继续跟进 getExecuteHandler,此时executeHandlers 中一个 org.noear.solon.serialization.snack 3.SnackActionExecutor,用于处理 json 请求
![[_resources/solon 框架请求处理流程分析/94de6414cc075fde9b95d4cfdeb63003_MD5.jpeg]]
但是这里需要 contentType 满足一定条件,最终定位到org.noear.solon.serialization.snack 3.SnackStringSerializer#label 这个变量,contentType 需要包含 /json 才可以使用 SnackActionExecutor 来进行处理,否则会调用getExecuteHandlerDefault 来获取默认的 Executor,也就是 org.noear.solon.core.mvc.ActionExecuteHandlerDefault

buildArgs

下一步会进入 executeHandle

1
2
3
4
5
@Override  
public Object executeHandle(Context ctx, Object obj, MethodWrap mWrap) throws Throwable {
List<Object> args = buildArgs(ctx, obj, mWrap);
return mWrap.invokeByAspect(obj, args.toArray());
}

会首先进行 buildArgs,用于处理请求的相关参数
调用 changeBody 来从 SmHttpContext 从获取 body
![[_resources/solon 框架请求处理流程分析/1cda61ba9c937792dc49f42569b5f251_MD5.jpeg]] 最终在org.noear.solon.boot.smarthttp.http.SmHttpContext#paramsMapInit 中进行处理,使用_request.getParameters()获取 body,最终返回一个 NvMap 类型。
接着会获取 controller 中请求参数的类型,并用 bodyObj 来进行转换,如果参数是以下类型,会直接进行获取,不会使用 bodyObj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (Context.class.isAssignableFrom(pt)) {  
//如果是 Context 类型,直接加入参数
//
args.add(ctx);
} else if (ModelAndView.class.isAssignableFrom(pt)) {
//如果是 ModelAndView 类型,直接加入参数
//
args.add(new ModelAndView());
} else if (Locale.class.isAssignableFrom(pt)) {
//如果是 Locale 类型,直接加入参数
//
args.add(ctx.getLocale());
} else if (UploadedFile.class == pt) {
//如果是 UploadedFile // args.add(ctx.file(p.getName()));
}

如果不是以上的类型,会继续调用 org.noear.solon.core.handle.Context#pull

1
2
3
4
5
6
7
8
9
10
11
12
public Object pull(Class<?> clz) {  
if (clz.isInstance(request())) {
return request();
}
if (clz.isInstance(response())) {
return response();
}
if (clz.isInstance(sessionState())) {
return sessionState();
}
return null;
}

如果还没有获取到,就会开始进入分支,先判断是否为 isRequiredBody,是的话会进行赋值,例如 Map 类型会直接将刚刚的 bodyObj 赋值过去

1
2
3
4
5
6
7
8
9
10
11
12
if (tv == null) {  
if (p.isRequiredBody()) {
//需要 body 数据
if (String.class.equals(pt)) {
tv = ctx.bodyNew();
} else if (InputStream.class.equals(pt)) {
tv = ctx.bodyAsStream();
} else if (Map.class.equals(pt) && bodyObj instanceof NvMap) {
tv = bodyObj;
}
}
}

如果不是 RequiredBody,则会调用 changeValue 进行参数的类型转换,把 bodyObj 转换成目标所需的类型

1
2
3
4
5
6
7
8
9
if (tv == null) {  
//尝试数据转换
try {
tv = changeValue(ctx, p, i, pt, bodyObj);
} catch (Exception e) {
String methodFullName = mWrap.getDeclaringClz().getName() + "::" + mWrap.getName() + "@" + p.getName();
throw new StatusException("Action parameter change failed: " + methodFullName, e, 400);
}
}

如果经过上述步骤还没拿到 tv,就会判断是否是基础类型,并赋值一些初始值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (tv == null) {  
//
// 如果是基类类型(int,long...),则抛出异常
//
if (pt.isPrimitive()) {
//如果是基本类型,则为给个默认值
//
if (pt == short.class) {
tv = (short) 0;
} else if (pt == int.class) {
tv = 0;
} else if (pt == long.class) {
tv = 0L;
} else if (pt == double.class) {
tv = 0d;
} else if (pt == float.class) {
tv = 0f;
} else if (pt == boolean.class) {
tv = false;
} else {
//
//其它类型不支持
//
throw new IllegalArgumentException("Please enter a valid parameter @" + p.getName());
}
}
}

changeValue

我们重点关注类型转换的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
protected Object changeValue(Context ctx, ParamWrap p, int pi, Class<?> pt, Object bodyObj) throws Exception {  
String pn = p.getName(); //参数名
String pv = p.getValue(ctx); //参数值
Object tv = null; //目标值
if (pv == null) {
pv = p.getDefaultValue();
}
if (pv == null) {
//
// 没有从 ctx.param 直接找到值
//
if (UploadedFile.class == pt) {
//1.如果是 UploadedFile 类型
tv = ctx.file(pn);
} else if (UploadedFile[].class == pt) {
//2.如果是 UploadedFile[] 类型
tv = Utils.toArray(ctx.files(pn), new UploadedFile[]{});
} else {
//$name 的变量,从attr里找
if (pn.startsWith("$")) {
tv = ctx.attr(pn);
} else {
if (pt.getName().startsWith("java.") || pt.isArray() || pt.isPrimitive()) {
//如果是java基础类型,则为null(后面统一地 isPrimitive 做处理)
//
tv = null;
} else {
//尝试转为实体
tv = changeEntityDo(ctx, p, pn, pt);
}
}
}
} else {
//如果拿到了具体的参数值,则开始转换
tv = changeValueDo(ctx, p, pn, pt, pv);
}
return tv;
}

第一种参数的获取方式是先获取参数的名字,并尝试从 ctx 中拿到对应的值

1
2
String pn = p.getName();        //参数名  
String pv = p.getValue(ctx); //参数值

例如我们请求中的参数 Map map,对应的 pn 就为 map,我们传参如果传入 map=1&a=222 就会被获取
第二种方式是 p.getDefaultValue(); 获取
第三种方式首先获取文件,如果是$开头就从 attr 里面找,如果参数类型是 java.开头就交给上一层的基础类型来处理
第三种方式中最终会使用 changeEntityDo 来还原对象,而通过前两种方式获取的会用 changeValueDo 来还原

changeValueDo

具体转换的逻辑在 org.noear.solon.core.util.ConvertUtil#to(org.noear.solon.core.wrap.VarDescriptor, java.lang.String, org.noear.solon.core.handle.Context)中
前面基本都是一些基本类型的转换,最后会进入 org.noear.solon.core.util.ConvertUtil#tryTo 中进行转换
这个函数用于将 String 值转换为别的类型

1
2
3
4
5
6
7
public static Object tryTo(Class<?> type, String val) {  
//尝试获取转换器
Converter converter = Solon.app().converterManager().find(String.class, type);
if (converter != null) {
return converter.convert(val);
}
......

会从converterManager 寻找合适的转换器,但默认配置下是空的,如果需要自定义转换器可以参考官方文档进行配置 https://solon.noear.org/article/567
这一部分暂时没有什么利用方式,进入 changeValueDo 之后基本就只能进行基本类型的转换

changeEntityDo

再来看第三种方式获取的参数值如何进行转换,只能处理那些 UploadFile 类型或者不为 java.开头的类型
例如下面这个路由,入参为 User

1
2
3
4
5
@Mapping("/addUser")  
@Post
public String addUser(User u) {
return u.getName();
}

会调用 org.noear.solon.core.wrap.ClassWrap#newBy(java.util.function.Function<java.lang.String,java.lang.String>, org.noear.solon.core.handle.Context)
并且调用 org.noear.solon.core.wrap.ClassWrap#doFill 去填充字段值,会将 String 的值转换成对应参数的类型,和上面的流程类似

SnackActionExecutor

我们可以引入依赖

1
2
3
4
<dependency>  
<groupId>org.noear</groupId>
<artifactId>solon-api</artifactId>
</dependency>

其中包含了

1
2
3
4
5
<!-- Json序列化支持组件 -->  
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon.serialization.snack3</artifactId>
</dependency>

用于处理 json 的序列化和反序列化,这也是官方默认配置的插件,可以参考 https://solon.noear.org/article/family-solon-serialization
我们发送以下数据

1
2
3
4
5
6
7
POST /api HTTP/1.1
Host: 192.168.13.1:8888
Connection: keep-alive
Content-Type: /json
Content-Length: 12

{"test":"2"}

changeBody

流程和上面一样,区别在于这里重写了 changeBody

1
2
3
4
@Override  
protected Object changeBody(Context ctx, MethodWrap mWrap) throws Exception {
return serializer.deserializeFromBody(ctx);
}

主要是 snack3 中的逻辑,会将我们 body 的字符串转换为 json 对象

changeValue

重写了 changeValue
进入 org.noear.snack.to.ObjectToer#analyse 来进行 json 反序列化
这里支持使用 org.noear.snack.to.ObjectToer#getTypeByNode 来指定目标类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
private Class<?> getTypeByNode(Context ctx, ONode o, Class<?> def) {  
//
// 下面使用 .ary(), .oby(), .val() 可以减少检查;从而提高性能
//
if (ctx.target_type == null) {
if (o.isObject()) {
return LinkedHashMap.class;
}

if (o.isArray()) {
return ArrayList.class;
}
}

String typeStr = null;
if(ctx.options.hasFeature(Feature.DisableClassNameRead) == false) {
if (o.isArray() && o.ary().size() == 2) {
ONode o1 = o.ary().get(0);
if (o1.isObject() && o1.obj().size() == 1) { //如果只有一个成员,则可能为list的类型节点
//
// 这段,不能与下面的 o.isObject() 复用
//
ONode n1 = o1.obj().get(ctx.options.getTypePropertyName());
if (n1 != null) {
typeStr = n1.val().getString();
}
}
}

if (o.isObject()) {
ONode n1 = o.obj().get(ctx.options.getTypePropertyName());
if (n1 != null) {
typeStr = n1.val().getString();
}
}
}

if (StringUtil.isEmpty(typeStr) == false) {
if(typeStr.startsWith("sun.") ||
typeStr.startsWith("com.sun.") ||
typeStr.startsWith("javax.") ||
typeStr.startsWith("jdk.")) {
throw new SnackException("Unsupported type, class: " + typeStr);
}

Class<?> clz = ctx.options.loadClass(typeStr);
if (clz == null) {
throw new SnackException("Unsupported type, class: " + typeStr);
} else {
return clz;
}
} else {
if (def == null || def == Object.class) {
if (o.isObject()) {
return LinkedHashMap.class;
}

if (o.isArray()) {
return ArrayList.class;
}
}

return def;
}
}

其中 ctx.options.getTypePropertyName() 结果就是 @type
但是需要关闭 Feature.DisableClassNameRead 这个属性,才可以从 body 中获取 @type
后续会取到属性值进行 setValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void setValue(Object tObj, Object val, boolean useSetter) {  
if (readonly) {
return;
}

try {
if (_setter != null && useSetter) {
_setter.invoke(tObj, new Object[]{val});
} else {
if (field.isAccessible() == false) {
field.setAccessible(true);
}

field.set(tObj, val);
}
} catch (IllegalArgumentException ex) {
if (val == null) {
throw new IllegalArgumentException(field.getName() + "(" + field.getType().getSimpleName() + ") Type receive failure!", ex);
}

throw new IllegalArgumentException(
field.getName() + "(" + field.getType().getSimpleName() +
") Type receive failure :val(" + val.getClass().getSimpleName() + ")", ex);
} catch (IllegalAccessException e) {
throw new SnackException(e);
} catch (RuntimeException e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}

useSetter 属性默认是 false,没有办法调用,默认只能使用反射去设置属性值,如果利用sun.print.UnixPrintServiceLookup 这个类就可能产生 rce 风险