技术考古:同一个 Maven 仓库里,Struts 1.2.6 与 Struts 2.2.1 如何共存?

39 阅读8分钟

老炮踩坑录 · yns-F01 · 翻车现场系列

基于某运营商语音系统仓库实读源码,两代 Struts 的证据逐条附行号

关键词:Struts 1.2.6 · Struts2 2.2.1 · ActionServlet · StrutsPrepareAndExecuteFilter · 依赖考古

👋 欢迎阅读 struts-csdn-fm.png 🏠个人主页:知守观
📘我的专栏: 老炮踩坑录
💻当前内容:一个仓库两代 Struts

引言

先交代背景。这是一个外包项目,某运营商的语音增值系统, 报警、 门户、数据迁移、GR 服务都在一个聚合工程里,一共 10 个模块。按发文惯例,甲方和外包方的名字不出现,代码里的包名我做了脱敏,com.xxx 开头的都是同一个真实包名替换的。文件路径和配置内容是原样的。

离职后整理老项目,我把它从备份里翻出来跑了一遍依赖扫描。结果里最扎眼的是两行:apache-struts:struts:1.2.6 和 org.apache.struts:struts2-core:2.2.1,在同一个聚合工程里。

Struts 1.2.6 是 2004 年前后的版本,Struts2 2.2.1 是 2010 年前后的版本。两个时代的 Web 框架,在同一个 Maven 聚合里各管一摊,各自跑在生产上。这篇把这个仓库拆开来,看看两代框架是怎么共存的,各自的写法长什么样,以及安全水位到底落在哪。

仓库先摆出来:10 个模块,编译级别 JDK 1.6

顶层 pom 的 modules 节(L14-25):

ynsserver
ynsservice-parent
yns-portal        # 门户
migration-server  # 数据迁移
yns-gfep
yns-alarm         #  报警
yns-vsms
grserver-war
grserver-core     # GR 服务
yns-mynewno

ynsservice-parent/pom.xml L84-97 里,maven-compiler-plugin 的编译级别:

<source>1.6</source>
<target>1.6</target>

JDK 1.6。这个数字先记住,后面讲整改成本的时候会用到。

两代 Struts 的证据

报警模块,yns-alarm/pom.xml L40-45:

apache-struts:struts:1.2.6

门户模块 yns-portal/pom.xml L40-140,同一个 1.2.6,外加 ibatis-sqlmap 2.3.4.726。

数据迁移模块,migration-server/pom.xml L23-44:

org.apache.struts:struts2-core:2.2.1
org.apache.struts:struts2-convention-plugin:2.2.1
org.apache.struts:struts2-spring-plugin:2.2.1

同一个聚合工程,三个 Web 模块,一边两个用 Struts 1.2.6,一边一个用 Struts2 2.2.1。聚合 pom 把它们收在一个反应堆里了,版本没人统一管。

Struts 1.x 侧:ActionServlet、*.do,再加一层自制代理

报警模块的入口配置,yns-alarm/web/WEB-INF/web.xml L59-80(节选):

<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
...
<param-name>config</param-name>
<param-value>/WEB-INF/struts-alarm.xml</param-value>
...
<url-pattern>*.do</url-pattern>

ActionServlet 加 *.do,Struts 1 的标准入口。再看 struts-alarm.xml L4-51 的 action 配置(包名已脱敏):

<action-mappings>
    <action path="/acs/login"     type="com.xxx.umcp.comm.proxy.ynsActionProxy" parameter="method" ... />
    <action path="/acs/parse"     type="com.xxx.umcp.comm.proxy.ynsActionProxy" parameter="method" ... />
    <action path="/acs/alarmMgt"  type="com.xxx.umcp.comm.proxy.ynsActionProxy" parameter="method" ... />
    <action path="/parammgt/paramRetrieve" type="com.xxx.umcp.comm.proxy.ynsActionProxy" parameter="method" ... />
</action-mappings>

注意到没有,所有 action 的 type 都是同一个类:ynsActionProxy。业务 Action 在配置文件里一个都不出现。

这个代理类我找到了源码,它挂在另一个仓库一个叫 logutil 的模块里(一个名字叫日志工具、实际装着全系统 Web 入口代理的模块,模块命名这件事本身又能写一篇)。它的结构(按 L36-105 实读整理,省略细节):

public class YnsActionProxy extends DelegatingActionProxy {
    // L36-66:从 Spring 的 WebApplicationContext 里按 bean 名取委托 Action
    // L84-105:execute(mapping, form, request, response) 里读 parameter 传进来的
    //          method 参数,反射调用委托对象上的同名方法
}

Struts 1 官方给的多方法分发方案是 DispatchAction,这个项目没用,自己在 Spring 的 DelegatingActionProxy 上又加了一层:配置里的 parameter="method" 指定方法名,代理类拿到名字后反射调到 Spring 容器里的业务对象上。

等于在 Struts 1 里面手搓了一套后来 Struts2 才原生支持的东西——一个 URL 映射到类的任意方法。需求是真实的,只是实现的代价是:所有请求都要过这层反射分发,类型检查帮不上忙,方法名写错一个字母,运行期才炸。

Struts2 侧:Filter 入口、*.action、注解配置

migration-server 是另一番写法。入口在 migration-server/src/main/webapp/WEB-INF/web.xml L45-67(节选):

<filter-class>org.apache.struts2.dispatcher.ActionContextCleanUp</filter-class>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
...
<url-pattern>*.action</url-pattern>

*.do 对 *.action,Servlet 对 Filter,光看 URL 后缀就能分清一个请求落在哪个时代。

struts.xml L2-14 里两行关键配置:

<constant name="struts.objectFactory" value="spring" />
<!-- convention 插件的 base package 和上传大小限制也在这个文件里 -->

Action 类不走 xml 配置,走 convention 插件的注解。migration/action/basiccfg/BasicAction.java L45-89:

@Namespace("/basiccfg")
public class BasicAction extends BaseAction {
​
    @Action(value = "/doGetConfig", ...)
    public String doGetConfig() {
        ...
    }
}

BaseAction L38 一行:public class BaseAction extends ActionSupport。

这边是 Struts2 的标准写法:ActionSupport、注解 namespace、注解 action,方法返回 String 当逻辑视图名。没有 ActionForm,没有 struts-config 里的 type 全限定名,也没有自制代理层。同一个团队的后来的模块,写法完全是另一个时代。

安全水位:两边都在漏洞影响范围里,只是区间不同

版本事实我先摆出来,公告口径以官方为准。

Struts 1.2.6 这一侧。Struts 1.x 整个产品线 2013 年就宣布停止维护了,最后的版本停在 1.3.10。1.2.6 连 1.x 的最后一个修复版本都没挨到。2014 年公开的 CVE-2014-0114(ClassLoader 操纵)覆盖 1.x 全线,1.2.6 在影响范围里。同模块还躺着 commons-fileupload 1.2.1、log4j 1.2.14,这两个后来的 CVE 清单各自都能列一串。

Struts2 2.2.1 这一侧。2011 到 2013 年那波 OGNL 远程执行公告——S2-009、S2-012 到 S2-016 这一串——影响的版本区间都把 2.2.1 包在里面。2017 年那个闹得最大的 S2-045,官方公告的影响范围从 2.3.5 起算,2.2.1 不在列表里。不在列表里不代表安全,是这个版本老到连确认它受不受影响这件事都没人做了。

一边是被官方放弃整条产品线,一边是躺在高危公告影响范围的正中间。两个模块在同一个仓库里,谁也没比谁安全。

为什么同一个仓库会搞成这样

我不在这个项目了,只能按代码和结构推断,说错的地方欢迎当时的同事指正。

从模块划分看, 报警和 门户是一期,数据迁移是后加的。一期立项的时候 Struts 1 还是主流,等做迁移模块的时候,Struts2 已经成了新项目的默认选项。每个模块立项时都选了"当下主流",没人有动机去动已经在跑的旧模块——报警门户的页面还能用,重写一遍的收益说不清楚,风险全是自己的。

聚合 pom 只管把模块收进一次构建,不管统一版本。这跟我在 S01 里写的 fastjson 是同一个原因:版本决策点散在各模块手里,没有机制保证它们指向同一个时代。

整改的真实成本

如果真要把这两代框架收掉,账是这么算的。

Struts2 2.2.1 升 2.5.x,官方给了迁移路径,convention 插件和 Spring 插件都还在,改配置、换常量名、过一遍拦截器,量级是周。但第一道坎不是 Struts,是 JDK——编译级别 1.6,Struts2 新版本要求更高的 Java 级别,升框架之前得先升 JDK,升 JDK 又得先过一遍所有依赖。

Struts 1.x 这两个模块没有升级路径。1.x 到 Struts2 不是升版本,是换框架:ActionForm 体系、struts-config、那层自制代理,全都要推倒。两个选项:重写,或者隔离下线—— 报警门户这种页面量不大的模块,重写一遍 JSP 加 Action 反而比研究怎么平滑迁移便宜。页面量我没细数,想接这个活的可以自己 grep 一下 jsp 文件数。

对照表

image.png

有件事如实交代:ynsActionProxy 的源码不在 yns-service 这个聚合工程里,在兄弟仓库一个叫 yns-logutil 的模块里。 模块对它的依赖关系我没完整追,jar 是从哪个坐标引进来的,我翻了但没有全部核实。另外所有 CVE 的版本区间以官方公告为准,我引用的是公开资料的通行说法。

自查打卡:在你的仓库根目录跑: grep -rn "struts" --include=pom.xml | grep -i version
再跑:
find . -name "struts*.xml"
看看跳出来几个时代的产物?打在评论区,我看看谁还在维护“上古神器”。

老炮点评

这个仓库最值钱的地方,是两代框架并排摆在那里,你能直接看到十年里每次技术选型留下的痕迹。一期的 Struts 1、后来的 Struts2、自制代理层里那个提前于框架的方法分发需求,每一层都对应着当时的一个合理决定。

老项目的技术栈没人画过设计图,它是一年一年沉积出来的。每个模块立项时选的都是当下主流,叠加十年,就成了考古现场。这类仓库里真正危险的不是老,是没人说得清哪个模块跑的是哪个版本——问就是"能跑",一扫全在漏洞影响范围里。

最后问一句

你维护过的最老的 Web 框架是哪个版本?有没有人还在跑 Struts 1?
遇到这种完全没法平滑升级的模块,你们老板是选择花钱重写,还是选择“贴个创可贴”继续跑?评论区聊聊。

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注 | 💬 留言

欢迎留言探讨。你的每一次互动,都是我继续更新的动力🚀


下期预告:《数据库地址和密码,写在 properties 的注释里》

还是这个仓库。vvm.properties 的注释里躺着完整的 Oracle 连接串和账号密码,ynsclient.properties 里 RAC 地址列表、数据源密码、远程服务 URL 俱全。密钥扫描工具盯着 yml 和 java 文件,注释通常不在扫描范围——这个盲区里能挖出多少东西,下一篇逐行过。

我是老炮,18 年 Java 老兵,仍在一线。关注我的掘金「Java老炮踩坑录」,真实项目复盘,让你少走弯路。