目录
0 背景
对学习的知识做复习总结,也是到处找文章学的,编写内容可能是大段复制过来,尽可能表明出处,若有遗漏请担待,侵权即删。
本次的环境是struts2-core-2.0.6,主要是想了解struts2的基本框架执行流程和struts2审计的基础知识。(一开始写了一顿2.5.13的,后来发现漏洞版本比较低,重写一版吧)
参考资料
1 struts2基本框架及入门
1.1 struts2概述
struts2相当于SSM架构中的springmvc,主要负责web层,作为控制器来建立模型与视图之间的数据交互。其核心内容包括:interceptor、action和valuestack。
1.2 servlet和struts2
servlet的一次请求流程如下:
Web容器(中间件,或许都已Tomcat为例)负责Socket连接、封装request和response,以及管理Servlet。其通过注解或web.xml文件配置过滤器Filter和Servlet处理类,将路径和具体的Filter和Servlet处理类关联起来。此外,像jsp文件的动态加载,解析成.java文件都是由Tomcat等中间件完成的,所以它们有自己的一套类加载机制。web.xml是Web应用相关的配置文件,主要用来配置过滤器、监听器、Servlert的路径匹配等信息,Tomcat通过这个文件初始化配置。
struts2的一次请求流程如下:
相比于Servlet,Struts2架构增加了一个统一调度中心——FilterDispatcher(核心过滤器,不同版本可能不同),并通过一系列的默认Struts2拦截器对请求进行处理,最后找到路径对应的Action来处理请求,也可以简单地把Action看作一个Servlet。
struts.xml是Struts2架构的配置文件,用来配置拦截器、Action路径匹配等。
1.3 struts2案例——登录
要实现一个基本的struts2案例,主要完成三步工作:web.xml配置核心过滤器信息、struts.xml配置Action和过滤器信息、创建相应Action类、jsp页面。
(0)项目创建
Java:1.8;IDEA2022;Tomcat9。PS:Java版本过高需要通过Jakarta创建项目,我这里的是通过Java Enterprise创建的;两者无太大差别,但包名有所不同。Tomcat版本不易过高,我一开始用tomcat10没办法部署,改了9可以。
配置Tomcat并运行:
(1)struts2依赖和web.xml配置
struts2核心过滤器StrutsPrepareAndExecuteFilter本质仍然是一个Filter,需要在web.xml中配置,以便Web容器找到。
<filter></filter>:配置过滤器的名字和Filter处理类在哪;<filter-mapping>:配置过滤器过滤哪些路径(作用域);两者通过<filter-name>关联起来,共同作用使一个Filter生效。例如在上述例子中,定义了一个名为struts2的过滤器实例,其具体的过滤器处理类全限定名为org.apache.struts2.dispatcher.filter.FilterDispatcher(不同版本可能不一样),其匹配的路径为/*(即全部路径)。所以上述的功能就是所有请求都经过StrutsPrepareAndExecuteFilter过滤器。
(2)struts.xml配置
struts.xml的struts2框架的核心配置文件,其定义具体路径-处理类、以及返回页面的关系,以及配置一些struts2的全局变量。下图表示,当请求/login的时候,会调用com.example.ss001.action.LoginAction的login方法来处理。如果login方法返回success或error,则返回/success.jsp或login.jsp页面。
(3)相关jsp页面和action处理类
login.jsp
LoginAction
success.jsp
index.jsp
运行结果:
1.4 Action详解
struts2框架中的Action主要是用来完成业务逻辑的操作,相当于Servlet和SpirngMVC中的Controller等。
1.4.1 创建Action类
创建Action类的常见方式:普通类创建,类实现Action接口,类继承ActionSupport类。
(1)普通类创建:在上述的登录案例就是这种方式,所有内容自己定义和实现。
(2)实现Action接口
package com.opensymphony.xwork2;
public interface Action {
String SUCCESS = "success";
String NONE = "none";
String ERROR = "error";
String INPUT = "input";
String LOGIN = "login";
String execute() throws Exception;
}
Action接口定义了集中返回常量类型和一个execute抽象方法。
(3)继承ActionSupport类
这是最常用的方法,ActionSupport类实现了Action接口,也实现了一些列的方法,可以方便使用。
1.4.2 访问Action
创建Action其实怎样都不所谓,重点在于业务的实现。要实现Action的访问需要在struts.xml中做好配置。一般来说,有两种访问方式:
1)通过<action>标签来配置响应的Action处理类,method方法指定调用方法。如果没有method则默认访问execute方法。一般来说是单对单的模式,一个<action><method>标签匹配一个类方法
2)通过通配符*简化配置。
例如一个BookAction类:
public class BookAction {
public void add() {
System.out.println("BookAction add()");
}
public void del() {
System.out.println("BookAction del()");
}
public void update() {
System.out.println("BookAction update()");
}
public void find() {
System.out.println("BookAction find()");
}
}
通过方式1)的struts.xml如下:
<action name="book_add" class="party.laucloud.web.action.BookAction" method="add"></action>
<action name="book_del" class="party.laucloud.web.action.BookAction" method="del"></action>
<action name="book_update" class="party.laucloud.web.action.BookAction" method="update"></action>
<action name="book_find" class="party.laucloud.web.action.BookAction" method="find"></action>
通过方式2)的struts.xml如下: {1}和*配套使用,{1}代表第一个通配符的内容
<action name="book_*" class="party.laucloud.web.action.BookAction" method="{1}"></action>
1.5 数据封装
数据封装解决的问题是,Action如何获取前端传过来的数据。一般包括:属性驱动和模型驱动两种方式。实际上,具体封装处理是在interceptor里面完成,详细流程可看第三部分。
1.5.1 属性驱动
(1)直接型
直接在Action类中定义相应的属性变量,并提供get/set方法(和.net很像)。
login1.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/login" method="post">
username:<input type="text" name="username" /><br>
password:<input type="password" name="password" /><br>
<input type="submit" name="login" />
</form>
</body>
</html>
LoginAction.java:
public class LoginAction {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String login() {
System.out.println("LoginAction login()");
System.out.println(username + " " + password);
if ("tom".equals(username) && "123".equals(password)) {
return "success";
} else {
return "failer";
}
}
}
(2)间接型
先通过定义一个实体类,然后通过该该实体类封装属性。实体类一般单独作为一个类,放在pojo文件夹下,这里为了方便显示,就在放在Action里了。既要改变jsp又要改变Action
login2.jsp:这里的name使用了user.的形式。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/login2" method="post">
username:<input type="text" name="user.username" /><br>
password:<input type="password" name="user.password" /><br>
<input type="submit" name="login" />
</form>
</body>
</html>
LoginAction2
public class LoginAction2 {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String login() {
System.out.println(user.getUsername() + " " + user.getPassword());
if ("tom".equals(user.getUsername()) && "123".equals(user.getPassword())) {
return "success";
} else {
return "failer";
}
}
}
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
1.5.2 模型驱动
模型驱动的重点在于改变Action的编写。如实现ModelDriven接口、实例化javaBean、重写getModel返回javaBean。
LoginAction3
public class LoginAction3 implements ModelDriven<User> {
private User user = new User();
@Override
public User getModel() {
return user;
}
public String login() {
System.out.println(user.getUsername() + " " + user.getPassword());
if ("tom".equals(user.getUsername()) && "123".equals(user.getPassword())) {
return "success";
} else {
return "failer";
}
}
}
2 struts2执行流程
2.1 struts2初始化
当Tomcat启动时,会从web.xml中将struts2核心过滤器加入FilterChain中,并逐个调用Filter的init()方法。struts2的工作起点是其核心过滤器FilterDispatcher,其init()方法如下。
init()主要做了一些变量的初始化,以供doFilter()方法处理请求的时候使用。Dispatcher对象是用来封装配置信息,以便后续doFilter的时候进行处理。
2.1.1 this.filterConfig = filterConfig;
filterConfig是Filter的配置类,可以用来获取web.xml中Filter的配置信息。这个其实是Tomcat的内容,等到Tomcat源码分析的时候再讲吧。
2.1.2 this.dispatcher = this.createDispatcher(filterConfig);
调用this.createDisPatcher(filrerConfig)创建Dispatcher实例。这里的params是一个Map,封装了web.xml的信息,以键值对的形式存储起来。ServletContext是一个servlet上下文,也包含了web.xml的信息。这两个参数在第dispatcher.init的时候用到,主要用来配置日志和文件管理信息。
2.1.3 this.dispatcher.init();
这里主要用来解析各种配置文件,初始化相应的对象,并交给IOC容器管理。这些初始化变量都会存在configurationManager中。其实这些上下文或者配置啥的本质都是Map,然后把这些Map封装起来,并提供相应方式方便访问读取。
引用一张图(忘了出处了。。)
其中struts-default.xml声明了一大堆的interceptor,用来做请求处理。
2.1.4 this.pathPrefixes = this.parse(packages);
只是将相应的静态资源路径保存到pathPrefixes这个List<String>中。
2.2 请求访问执行流程
这里发起请求是利用1.3的登录案例作为调试演示。由于当服务器收到请求时,会调用过滤器的doFilter()方法,struts2核心过滤器的doFilter()如下。154-156行只是获取上下文和作用域对象。
2.2.1 UtilTimerStack.push(timerKey);
这里主要做一些时间和线程的处理(防止冲突),第一次访问的时候,会直接return出来。
2.2.2 request = this.prepareDispatcherAndWrapRequest(request, response);
对reques进行包装。
2.2.3 mapping = actionMapper.getMapping(request, this.dispatcher.getConfigurationManager());
configurationManager在初始化的时候存放了各种配置信息。跟进getMapping方法.
getUri():获取请求路径,在这里是/LoginAction.action。
dropExtension(uri):这里就是判断是否支持后缀请求,并将后缀去掉,也就是现在url变成了/LoginAction。
this.parseNameAndNamespace(uri, mapping, configManager);根据uri解析命名namespace和name。
this.handleSpecialParameters(request, mapping);获取并处理请求参数。
if (this.allowDynamicMethodCalls) : 由于此处获取的mapping不为空,会走else的逻辑,allowDynamicMethodCalls在2.0.6中是true,这个参数的作用是允许通过action!method的方式访问具体的方法,例如在这个例子中就是请求路径是/LoginAction!login,就可以利用LoginAction中的login方法来处理该请求。
2.2.4 this.dispatcher.serviceAction(request, response, servletContext, mapping);
由于mapping不为空,这里会直接进入190行的代码,也是doFilter最后的代码。前面的重点是找到action和处理action。继续跟进serviceAction()函数。具体如下:
2.2.4.1 Map<String, Object> extraContext = this.createContextMap(request, response, mapping, context);和ValueStack stack = (ValueStack)request.getAttribute("struts.valueStack");
将各种作用域和请求参数做封装整合,本质是一个Map。
ValueStack stack = (ValueStack)request.getAttribute("struts.valueStack");用于创建一个值栈ValueStack,ValueStack是一个数据中心,request中没有ValueStack所以这里会返回null。
2.2.4.2 ActionProxy proxy = ((ActionProxyFactory)config.getContainer().getInstance(ActionProxyFactory.class)).createActionProxy(namespace, name, extraContext, true, false);
这里创建一个代理类,用于后面代理invoke递归执行各种拦截器、处理action、处理返回结果等。并且会在这里创建一个OgnlValueStack,用来解析ognl表达式。
重点是在prepare()方法里面。config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName);主要是去创建响应请求的actionConfig信息:处理类的名字、方法、参数等等。
然后就是创建代理执行类invocation = new DefaultActionInvocation(objectFactory, unknownHandler, this, extraContext); 也会init()创建OgnlValueStack中,存放于invocation的stack中。并且ActionContext和ValueStack是你中有我我中有你。
resolveMethod();:着就是获取请求对应action的处理方法,如果请求是 ! 这种形式的话,actionProxy的method在后面的流程中会被赋值并再次执行 resolveMethod()方法;如果不是的话就从actionConfig中获取响应的方法,actionConfig对应struts.xml,如果在struts.xml中没有指定的方法,就会默认执行action的execute方法。
2.2.4.3 proxy.execute();
这里会开始调用代理类执行invoke方式,开始递归调用interceptor和action。
public String invoke() throws Exception {
String profileKey = "invoke: ";
try {
UtilTimerStack.push(profileKey);
if (executed) {
throw new IllegalStateException("Action has already executed");
}
if (interceptors.hasNext()) {
//1. 获取interceptor并递归调用
final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
UtilTimerStack.profile("interceptor: "+interceptor.getName(),
new UtilTimerStack.ProfilingBlock<String>() {
public String doProfiling() throws Exception {
resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
return null;
}
});
} else {
// 2. 调用完interceptor后就调用action
resultCode = invokeActionOnly();
}
// this is needed because the result will be executed, then control will return to the Interceptor, which will
// return above and flow through again
if (!executed) {
if (preResultListeners != null) {
for (Iterator iterator = preResultListeners.iterator();
iterator.hasNext();) {
PreResultListener listener = (PreResultListener) iterator.next();
String _profileKey="preResultListener: ";
try {
UtilTimerStack.push(_profileKey);
listener.beforeResult(this, resultCode);
}
finally {
UtilTimerStack.pop(_profileKey);
}
}
}
// now execute the result, if we're supposed to
if (proxy.getExecuteResult()) {
// 3. 执行结果返回
executeResult();
}
executed = true;
}
return resultCode;
}
finally {
UtilTimerStack.pop(profileKey);
}
}
执行拦截器:resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);会遍历interceptors执行全部的拦截器,以ParameterFilterInterceptor为例,发现执行完自身过滤器逻辑后,会递归调用invocation.invoke(),直至遍历执行所有拦截器
执行action:resultCode = invokeActionOnly(); 反射调用执行Loginaction的login方法。
处理Result:executeResult();