老炮踩坑录 · yns-F01 · 翻车现场系列
基于某运营商语音系统仓库实读源码,两代 Struts 的证据逐条附行号
关键词:Struts 1.2.6 · Struts2 2.2.1 · ActionServlet · StrutsPrepareAndExecuteFilter · 依赖考古
👋 欢迎阅读
🏠个人主页:知守观
📘我的专栏: 老炮踩坑录
💻当前内容:一个仓库两代 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 文件数。
对照表
有件事如实交代: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老炮踩坑录」,真实项目复盘,让你少走弯路。