S2-S1插件工作原理研究

0x00 使用S2-S1插件

1.需求

对于Struts1的忠实粉丝,在Struts和webwork合并产生Struts2后,Struts1更新和维护就停止了。版本停留在1.3.10。而曾经保留的Action和ActionForm配置习惯还存在,想要移植也是一个麻烦的事情,所以官方写了一个S2-S1-plugin插件。提供的功能有:

  • Can use Struts 1 Actions and ActionForms with no code changes
  • Supports Commons Validator-enabled ActionForms
    大意是开发者可以使用Struts1中的Action和ActionForm,不需要改变其中的代码。并且支持验证表单。

2.使用

使用教程参考:http://struts.apache.org/docs/struts-1-plugin.html
使用插件所需的包在官方提供的app样例程序showcase中可以找到,并且需要将Struts1使用的包进行导入。
插件源码则可以在struts-2.3.24-allsrcplugins文件夹下找到。 写好Action和ActionForm代码后,只需要按照官方教程上写个Struts.xml配置文件即可。

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
<package name="integration" extends="struts1-default" namespace="/integration">
<interceptors>
<interceptor name="userForm" class="com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor">
<param name="className">integration.form.UserForm</param>
<param name="name">userForm</param>
</interceptor>

<interceptor-stack name="integration">
<interceptor-ref name="staticParams"/>
<interceptor-ref name="userForm"/>
<interceptor-ref name="modelDriven"/>
<interceptor-ref name="actionForm-reset"/>
<interceptor-ref name="basicStack"/>
<interceptor-ref name="workflow"/>
</interceptor-stack>
</interceptors>

<default-interceptor-ref name="integration"/>
<default-action-ref name="inputUser"/>

<!-- Display entry page that uses Model-Driven technique -->
<action name="inputUser" class="org.apache.struts2.s1.Struts1Action">
<param name="className">integration.action.InputAction</param>
<result>/modelDriven.jsp</result>
</action>

<!-- Display the result page whose content is populated using the Model-Driven technique -->
<action name="loginUser" class="org.apache.struts2.s1.Struts1Action">
<param name="className">integration.action.LoginAction</param>
<result name="success">/ok.jsp</result>
<result name="error">/error.jsp</result>
</action>


</package>

0x01 插件工作原理分析

1.Struts1Action 中间代理

1
2
3
4
<action name="inputUser" class="org.apache.struts2.s1.Struts1Action">
<param name="className">integration.action.InputAction</param>
<result>/modelDriven.jsp</result>
</action>

在配置Struts.xml文件中,可以看到是Struts2规范。并将action设置为Struts1Action,而并非我们自定义的处理类integration.action.InputAction。并且自定义类当做className属性传入Struts1Action类中。我们看Struts1Action的主要属性有:

1
2
3
4
5
6
private ActionForm actionForm;
private String className;
private boolean validate;
private String scopeKey;
private ObjectFactory objectFactory;
private Configuration configuration;

其中className属性就是上述配置的自定义的Struts1 action类中integration.action.InputAction类,用于处理用户表单数据。该插件相当于封装了一个中间action,转发Struts2的action到Struts1的action进行处理。具体看看 execute()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
ActionContext ctx = ActionContext.getContext();
ActionConfig actionConfig = ctx.getActionInvocation().getProxy().getConfig();
Action action = null;
try {
action = (Action) objectFactory.buildBean(className, null);
} catch (Exception e) {
throw new StrutsException("Unable to create the legacy Struts Action", e, actionConfig);
}
Struts1Factory strutsFactory = new Struts1Factory(Dispatcher.getInstance().getConfigurationManager().getConfiguration());
ActionMapping mapping = strutsFactory.createActionMapping(actionConfig);
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
ActionForward forward = action.execute(mapping, actionForm, request, response);

在执行过程中,org.apache.struts2.s1.Struts1Action类首先
action = (Action) objectFactory.buildBean(className, null);,这样通过配置的className生成了一个Struts1 Action该类的java对象。
然后在ActionForward forward = action.execute(mapping, actionForm, request, response);这句代码,将actionForm的数据交给我们配置的Struts1 action进行处理。在这里很好的完成了中间代理连接Struts2和1的任务。 这里有个问题,对于表单和ActionForm属性间的传递是怎样实现的?
在Struts1中,通过ActionServlet中processPopulate(request, response, form, mapping);进行表单和ActionForm进行对应赋值。而这里则是通过配置的第二个关键技术ScopedModelDrivenInterceptor出路,内部使用OGNL语言进行表单数据处理对应。

2.ScopedModelDrivenInterceptor 拦截器

1
2
3
4
<interceptor name="userForm"    class="com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor">
<param name="className">integration.form.UserForm</param>
<param name="name">userForm</param>
</interceptor>

在Struts2中,会使用拦截器对数据进行分析处理,而ScopedModelDrivenInterceptor实现了将ActionForm传入OGNL执行所需要的Context上下文的操作,然后在参数拦截器中将数据对ActionForm进行赋值。
在拦截器执行的intercept方法中,Object model = resolveModel(objectFactory, ctx, cName, scope, modelName);是关键执行该功能的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Object resolveModel(ObjectFactory factory, ActionContext actionContext, String modelClassName, String modelScope, String modelName) throws Exception {
Object model = null;
Map<String, Object> scopeMap = actionContext.getContextMap();
if ("session".equals(modelScope)) {
scopeMap = actionContext.getSession();
}

model = scopeMap.get(modelName);
if (model == null) {
model = factory.buildBean(modelClassName, null);
scopeMap.put(modelName, model);
}
return model;
}

resolveModel函数首先实例化了ActionForm(这里的modelClassName),然后添加到了ContextMap中。这样会在执行OGNL语句中,会根据上下文进行赋值,顺利将表单中的参数赋值到ActionForm中。而且将model进行了

0x02 Plugin的调试过程

  • 对我写下的测试实例进行调试,在ScopedModelDrivenInterceptor拦截器和Struts1Action部分下断点调试。可以看到,首先进入拦截器,将ActionForm加入Context中。
  • 调试的时候,看到了OgnlContext的变化。
  • 然后将ActionForm加入上下文后,通过参数拦截器进行赋值操作。可以在newStack栈中看到刚才加入的UserForm。
  • 最后进入Struts1Action中,执行Struts1的action。

0x03 测试实例

S2-S1-Plugin测试工程文件

文章作者: angelwhu
文章链接: https://www.angelwhu.com/paper/2015/07/07/research-on-working-principle-of-s2s1-plugin/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 angelwhu_blog