简介: 去年的Log4j-core的安全问题,再次把供应链安全推向了高潮。在供应链安全的场景,蚂蚁集团在静态代码扫描平台-STC和资产威胁透视平台-哈勃这2款产品在联合合作下,优势互补,很好的解决了直接依赖和间接依赖的场景。但是由于STC是基于事前,受限于扫描效率存在遗漏的风险面,而哈勃又是基于事后,存在修复时间上的风险。基于此,笔者尝试寻找一种方式可以同时解决2款产品的短板。

作者 | 唐天龙(唐礼)
来源 | 阿里开发者公众号

一 背景

为什么想写此文

去年的Log4j-core的安全问题,再次把供应链安全推向了高潮。在供应链安全的场景,蚂蚁集团在静态代码扫描平台-STC和资产威胁透视平台-哈勃这2款产品在联合合作下,优势互补,很好的解决了直接依赖和间接依赖的场景。

但是由于STC是基于事前,受限于扫描效率存在遗漏的风险面,而哈勃又是基于事后,存在修复时间上的风险。基于此,笔者尝试寻找一种方式可以同时解决2款产品的短板。笔者尝试研究了一下Maven是如何处理一个项目中的直接依赖和间接依赖的,并且在遇到相同依赖时,Maven是如何进行抉择的,这里的如何抉择其实就是Maven的仲裁机制。带着这些问题,笔者尝试调研了Maven的源码和做了一些本地的测试实验。总结了这篇文章。

坐标是什么?

在空间坐标系中,我们可以通过xyz表示一个点,同样在Maven的世界里,我们可以通过一组GAV在依赖的世界里明确表示一个依赖,比如:

< groupId> : com.alibaba 一般是公司的名称

< artifactId> : fastjson 项目名称

< version> : 1.2.24 版本号

影响依赖的标签都有哪些

1.< dependencies>

直接引入具体的依赖信息。注意是不在< dependencyManagement>标签内的情况。如果是在< dependencyManagement>内的情况,请参考2号标签。

2.< dependencyManagement>

只声明但不发生实际引入,作为依赖管理。依赖管理是指真正发生依赖的时候,再去参考依赖管理的数据。

  • 这样使用dependency的时候,可以缺省version。
  • 另外< dependencyManagement> 还可以管控所有的间接依赖,即使间接依赖声明了version,也要被覆盖掉。

3.< parent>

声明自己的父亲,Maven的继承哲学跟Java很类似,因为Maven本身也是用Java实现的,满足单继承。

  • 一旦子pom继承了父pom,那么会把父pom里的 < dependencies> ,< dependencyManagement>等等属性都继承过来的。当然如果在继承的过程中,出现一样的元素,也是子去覆盖父亲,和Java类似。
  • 继承时,会分类继承。dependencies继承dependencies,dependencyManagement里的依赖管理只能继承dependencyManagement范围内的依赖管理。
  • 每一个pom文件都会有一个父亲,即使不声明Parent,也会默认有一个父亲。和Java的Object设计哲学类似。后面在源码分析中我们还会提到。

4.< properties>

代表当前自己的项目的一个属性的集合。

properties仅仅代表属性的声明,一个属性声明了,和他是否被引用并无关系。我完全可以声明一系列不被人使用的属性。

依赖的作用域都有哪些

一个依赖在引入的时候,是可以声明这个依赖的作用范围的。比如这个依赖只对本地起作用,比如只对测试起作用等等。作用域一共有compile,provided,system,test,import,runtime 这几个值。

简单总结一下:

  • compile和runtime会参与最后的打包环节,其余的都不会。compile可以不写。
  • test只会对 src/test目录下的测试代码起作用。
  • provided是指线上已经提供了这个Jar包,打包的时候不需要在考虑他了,一般像serlvet的包很多都是provided。
  • system和provided没什么太大的区别。
  • import只会出现在dependencyManagement标签内的依赖中,是为了解决Maven的单继承。引入了这个作用域的话,maven会把此依赖的所有的dependencyManagement内的元素加载到当前pom中的,但不会引入当前节点。如下图,并不会引入fastjson作为依赖管理的元素,只是会把fastjson文件定义的依赖管理引入进来。

二 单个Pom树的依赖竞争

Pom文件本质

一个Pom文件的本质就是一棵树。

在人的视角来观察一个Pom文件的时候,我们会认为他是一个线状的一个依赖列表,我们会认为下图的Pom文件抽象出来的结果是C依赖了A,B,D。但我们的视角是不完备的,Maven的视角来看,Maven会把这一个Pom文件直接抽象成一个依赖树。Maven的视角能看到除了ABD之外的节点。而人只能看到ABD三个节点。

既然是在一棵树上,那么相同的节点就必然会存在竞争关系。这个竞争关系就是我们提到了仲裁机制。

Maven仲裁机制原则

1.依赖竞争时,越靠近主干的越优先。

2.单颗树在依赖在竞争时(dependencies)(注意:不是dependencyManagement里的dependencies):

当deep=1,即直接依赖。同级是靠后优先。

当deep>1,即间接依赖。同级是靠前优先。

3.单颗树在依赖管理在竞争时(注意:是dependencyManagement里的dependencies)是靠前优先的。

4.maven里最重要的2个关系,分别是继承关系和依赖关系。我们所有的规律都应该只从这2个关系入手。

下图中分别是2个子pom文件(方块代表依赖的节点,A-1 表示A这个节点使用的是1版本,字母代表节点,数字代表版本)。

左边这个子pom生成的树依赖了 D-1,D-2和D-5。满足依赖竞争原则1,即越靠近树的左侧越优先的原则,所以D-5会竞争成功。

但是B-1和B-2同时都位于树的同一深度,并且深度为1,由于B-2更加靠后,所以B-2会竞争成功。

右边的子pom生成的树依赖了 D-1和D-2,并且位于同一深度,但由于D-1和D-2是属于间接依赖的范围,deep大于1,所以是靠前优先,那么也就是D-1会竞争成功。

常见场景

看到这里,想必大家已经了解了Maven的仲裁原则。但是在实际的工作中,光有原则还需要在代码中可以灵活的运用才能有属于自己的理解,这里笔者准备了5个场景,每个场景对应的答案都在后面,大家阅读时,可以自己尝试用Maven的原则来去推理,看看有没有哪里不符合预期的情况。

场景一 难度(☆)

场景描述

主POM里有< fastjson.version> 这个属性为1.2.24。

父亲是spring-boot-starter-parent-3.13.0。父亲里的< fastjson.version>是1.2.77。

并且在主pom中,消费了这个属性。

那么针对主POM这颗树,他最终会是使用哪一个fastjson呢?

场景示例


结构图

场景二 难度(☆☆)

在同一个主POM或者子POM中的dependencies中同时使用了Fastjson,第一个声明了1.2.24的版本,第二个声明了1.2.25版本。那么针对主POM或者子pom这棵树,最终会选择fastjson 1.2.24还是1.2.25呢?

场景示例

结构图

场景三 难度(☆☆☆)

下图中左图为主POM文件内的dependencyManagement里的fastjson为1.2.77,这个时候子POM中显示声明自己的版本1.2.78。那么针对子POM这颗树,子POM会选择听从父命还是遵从内心呢?

场景示例

结构图

场景四 难度(☆☆☆☆)

主POM的dependencies Fastjson:1.2.24 主POM的dependencymanagent Fastjson:1.2.77

主POM的父亲(springboot)的dependencies Fastjson 1.2.78

子POM里的dependencies Fastjson 1.2.25

这种情况下针对子pom来说,他会选择4个版本中的哪一个呢?

场景示例

结构图

场景五 难度(☆☆☆☆☆)

主POM的dependencies Fastjson:1.2.24 主POM的dependencymanagent Fastjson:1.2.77

主POM的父亲(springboot)的dependencies Fastjson 1.2.78

子POM里的dependencies 不写version

场景五跟场景四整体没有差别,只是将子pom的dependencies的版本进行缺省。

这种情况下针对子pom来说,针对子pom,他会选择3个版本中的哪一个呢?

场景示例

结构图

答案

场景一

1.2.24会最终生效。

因为子会继承父亲的属性,但是由于自己有这个属性,那么则覆盖!

继承一定会伴随着覆盖的,这个设计在编程语言中还是比较普遍的。

场景二

1.2.25会最终生效。

参考 单颗树在依赖在竞争时:当deep=1,即直接依赖。同级是靠后优先。

满足Maven的核心竞争依赖策略!

场景三

1.2.78最终会生效。

一个项目里的dependencyManagement只能对不声明version的dependency和间接依赖有效!

场景四

1.2.25会最终生效。这个比较复杂。

〇: 首先根据父子的继承关系,1.2.24会覆盖掉1.2.78。所以78版本淘汰

一: 由于一个项目里的dependencyManagement只能对不声明version的dependency和间接依赖有效,所以

1.2.77无法对1.2.25起作用。

二: 由于父子的继承关系,1.2.25会覆盖掉1.2.24.

所以最终1.2.25胜出!

场景五

1.2.77会最终生效。

〇: 首先根据父子的继承关系,1.2.24会覆盖掉1.2.78。所以78版本淘汰

一: 由于一个项目里的dependencyManagement是可以对不声明的version起作用,所以子pom的版本为1.2.77

二: 由于父子的继承关系,1.2.77会覆盖掉1.2.24.

所以最终1.2.77胜出!

三 多个Pom树合并打包

多棵树构建顺序原则

现在的项目一般都是多模块管理,会存在非常多的pom文件。多棵树的情况下每棵树的出场顺序都是事先已经被计算好的。

这个功能在Maven的源码中是一个叫Reactor(反应堆)实现的。它主要做了一件事情就是决定一个项目中,多个子pom谁先进行build的顺序,这个出厂顺序很重要,在合并打包时,往往决定了最终谁会在多个pom之间胜出的问题。

Reactor的原则

多棵树(多个子pom)构建的顺序是按照被依赖方的要在前,依赖方在后的原则。

项目要保证这里是不能出现循环依赖的。

Reactor的原则图解

如下图子pom1 在被子pom2和子pom3同时依赖,所以子pom1最先被构建,子pom3没有人被依赖,所以最后构建。

SpringBoot Fatjar打包的策略

SpringBoot 打包会打成一个Fatjar,所有的依赖都会放在BOOT-INF/lib/目录下。SpringBoot的打包是越靠后的构建pom越优先,因为一般会把springboot的打包插件放在最不被依赖的module里(比如上图里的Pom3)。(SpringBoot的打包插件一般放在bootstrap pom里,这个名字可以我们自己起,一般都是依赖关系最考上的module。在多模块管理的springboot应用内,bootstrap往往是最不被依赖的那个module。)

子pom3最后参与构建,而且SpringBoot打包插件一般打的就是这个module。所以最终进入到SpringBoot打包产物的有A-2,B-2,E-2,F-2和D-1。因为A-2和B-2相比于其他几个相同节点更靠近树的主干。E-2和F-2也是同理。这个规律体感上是靠后优先了,因为靠后的树天然更加靠近主干。

四 仲裁机制在Maven源码中的实现

以Maven的3.6.3版本的源码进行分析,我们尝试分析Maven中对依赖处理的几处原则,方能从源码的层面上正向的证明仲裁机制的准确性。另外从源码上也可以看出一些Maven上的机制为什么是这样,而不是单单的他的机制是什么样。因为笔者相信,任何机制都无法保证与时俱进下的先进性,所以笔者认为上文中提到的所有的仲裁机制有一天可能会发生变化,这些结论并非最重要,而是如何调研这些结论更为重要!

Maven是如何实现出继承并且相同属性子覆盖父的

Maven中有2条非常重要的主线。一个是依赖,另一个就是继承。Maven在源码中实现继承大体如下。在下图中使用readParent进行对父亲的模型获取之后,便让自己陷入这个循环中。唯一可以出去这个循环的方式就是追不到父亲为止。并且把每次取到模型数据放到linega这个对象当中。下图中最下面的assembleInheritance我们看他消费了linega这个对象,目的就是完成真实的继承和覆盖。

在assembleInheritance中我们会发现一个很有意思的现象,lingage是倒着进行遍历,并且是从倒数第二个元素开始,这正是上文中我们提到了的Maven的一个设计哲学。Maven认为这个世界上所有的pom文件都存在一个父亲,类似Java的Object。这里便是对这个哲学处理的一个浅逻辑。

另外Maven自上而下的去遍历,更加方便自己去实现相同的元素子覆盖父的能力,这也是笔者认为在编码上的一个小心思。

Reactor反应堆在源码中的实现

上文中我们还提到了一个非常重要的概念,就是反应堆。反应堆直接决定了各个子pom是如何决定构建顺序的。在Maven的源码中,他是在getProjectsForMavenReactor函数中进行实现的。并且我们从下图中也可以看到,Maven的反应堆是不能解决循环依赖的,他直接捕获了这种异常!

真正实现反应堆算法的是在ProjectSorter的构造函数中通过Dag进行实现的。Dag(有向无环图)和广度优先搜索是解决依赖场景是一个很好的方式。

在有向无环图中通过每次挑选出入度为0的节点,再删除该节点和此节点的相邻边,不断重复上述步骤。就可以高效率的计算出DAG上的所有节点的依赖顺序,Maven也正是用到了这个思路。

从这个源码的视角也可以解释为什么Maven必须要保证每一个子pom之前不能出现循环依赖。

同一个Pom文件内dependency 后声明的优先的实现

在处理Dependencies时,Maven并没有对此进行特殊处理,是直接使用的Map的方式进行覆盖的。关于这里为什么这么设计,笔者并不清楚。笔者曾一度猜测这么设计是为了让开发同学更好的编写,因为靠后优先往往符合大部分人的编码习惯。但是在这里我们看到了作者的一行注释,意思大概是说,这样设计是为了向后兼容Maven2.x,因为Maven2.x 是不会去校验一个文件是否只存在一个同GA的唯一依赖。所以后面的maven的版本应该也是延续了这种风格。

当循环进行处理到1.2.25的时候,依然进行对normalized这个map进行put操作导致了 key值相同的情况下的覆盖。

五 安全视角应如何避免间接依赖

分析

作为安全同学,笔者更希望的是针对这种多module的Maven项目可以梳理出一个经验,怎样去避免间接依赖的问题。

经过上面的分析,我们可以得出3条结论:

1.子pom声明版本在安全视角是非常危险的,子pom不应该显示声明版本。

由于子pom会继承主pom的元素,并且在继承的时候会出现覆盖的场景。那么针对CE或者SpringBoot打包时,有可能出现子pom的build的顺序位置天然非常有优势,容易造成子pom的版本进入最终的打包产物。

2.主POM的dependencyManagent可以管控到 间接依赖 和 不显示声明version的直接依赖。

3.主POM的dependencies不能出现危险版本。否则子pom天然的继承了这个危险版本参与打包。

结论

以上几条同时满足,便可以解决间接依赖的问题。
即:

针对SpringBoot而言,子pom不应该显示声明版本,主Pom的dependencyManagent应该管控安全版本的依赖,并且主pom不能出现危险版本。(主Pom dependencies强行写上安全版本更佳,这样可以避免掉依赖的父亲里存在残留的不安全的依赖)

六 最后

Maven的源码地址

https://archive.apache.org/dist/maven/maven-3/

我是怎么分析的

本人在本地针对SpringBoot,做多轮测试。在根目录下执行mvn clean package即可!

mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true 会帮助分析到具体的节点。

另外就是尝试在源码中找到这里的实现,这样更能加深理解!

常用的分析命令

0.mvn clean package -DSkipTest 直接进行打包,进行结果分析

1.mvn dependency:tree 会把整个的maven的树形结构输出

2.mvn help:effective-pom -Dverbose 这个命令输出的信息更加完整,输出的是effectivepom

3.mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true

4.mvn -D maven.repo.local =你的目录 compile阶段用到的依赖。

推荐阅读

1.如何写出一篇好的技术方案?

2.阿里10年沉淀|那些技术实战中的架构设计方法

3.如何做好“防御性编码”?


阿里云产品测评—开源PolarDB-PG

体验阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 的能力和高可靠、高可用、弹性扩展等企业级数据库特性。发布评测,写下你的感受与评价即可获得多重福利。

点击这里,查看详情。

本文为阿里云原创内容,未经允许不得转载。

安全同学讲Maven间接依赖场景的仲裁机制的更多相关文章

  1. Maven间接依赖冲突解决办法

    如果项目中maven依赖太多,由于还有jar之间的间接依赖,所以可能会存在依赖冲突.依赖冲突大部分都是由于版本冲突引起的,查看maven的依赖关系,可以找到引起冲突的间接依赖 如上图,通过Depend ...

  2. maven 间接依赖的jar自动引入

    很多时候,我们引用的第三方jar需要一些其他的第三方jar,这个时候默认情况下,间接需要依赖的第三方jar是不会自动被引入的,如果希望这些额外的三方jar被自动引入,则在Maven仓库中除了提交jar ...

  3. Maven快速入门(五)Maven的依赖管理

    前面我们讲了maven项目中的最重要的文件:pom.xml 配置文件相关内容.介绍了pom 是如何定义项目,如何添加依赖的jar 包的等. 我们知道,在Maven的生命周期中,存在编译.测试.运行等过 ...

  4. maven实现依赖的“全局排除”

    大多数java应用源码构建和依赖管理是使用maven来实现的,maven也是java构建和依赖管理的事实上的标准.我们的应用系统也都是基于maven构建的,maven虽然在依赖管理方面确实很牛叉,但是 ...

  5. 我用段子讲.NET之依赖注入其二

    <我用段子讲.NET之依赖注入其二> "随着我们将业务代码抽象化成接口和实现两部分,这也使得对象生命周期的统一管理成为可能.这就引发了第二个问题,.NET Core中的依赖注入框 ...

  6. maven的中传递依赖,maven的依赖管理(转)

    在maven的pom文件中 <dependencies> <dependency> <groupId>junit</groupId> <artif ...

  7. maven的依赖特性

    若排版紊乱可查看我的个人博客原文地址 maven的依赖特性很多很杂,这里大概总结一下,maven的依赖特性主要是依赖范围和传递依赖,前者会影响后者,这篇文章会介绍传递依赖的传递原则,出现冲突传递依赖默 ...

  8. Maven学习(十七)-----Maven外部依赖

    Maven外部依赖 正如大家所了解的那样,Maven确实使用 Maven 库的概念作依赖管理.但是,如果依赖是在远程存储库和中央存储库不提供那会怎么样? Maven 提供为使用外部依赖的概念,就是应用 ...

  9. maven项目依赖包问题

    问题 maven传递依赖 解决方案   前段时间,开发中遇到一个关于maven依赖包的问题:由于业务需要,支付网关对账代码中的slf4j-api包需要更新,原包为1.5.8版本,需要更新到1.6.4版 ...

  10. maven 学习---Maven外部依赖

    现在,你也知道Maven做依赖管理使用Maven仓库的概念.但是,如果依赖是不提供任何远程存储库和中央存储库发生了什么? Maven提供为使用外部依赖的概念,应用在这样的场景. 举一个例子,让我们做以 ...

随机推荐

  1. 玩转Vue3之深入理解响应式编程

    前言 Vue 3是一个功能强大的前端框架,它引入了一些令人兴奋的新特性,其中最引人注目的是ref和reactive.这两个API是Vue 3中响应式编程的核心,本文将深入探讨它们的用法和差异. 什么是 ...

  2. Git | Git Server 搭建,在自己的服务器上进行 git server 搭建

    系列文章目录 目录 系列文章目录 前言 操作 1. 创建 git 用户 2. 创建 .ssh 目录 3. 自定义仓库的根目录 4. 在服务器上创建个裸仓库 5. 手动配置一个公钥 6. 在本地测试一下 ...

  3. Android 获取设备的亮度百分比

    一般的屏幕亮度都是0-255,而小米手机的高版本不一样 为了使亮度调节更加细腻, MIUI对原生亮度级别进行了扩展, 由原有的255级调整根据不同屏幕分别支持255/1023/2047/4095级.开 ...

  4. Handler屏障消息

    Handler 屏障消息 Handler Message 种类 Handler的Message种类分为3种: 普通消息 屏障消息 异步消息 同步消息 我们默认用的都是同步消息,即前面讲Handler里 ...

  5. 智慧党建3D可视化方案,扩大党建文化宣传数字网络影响力

    信息技术的快速发展加快了社会分化解构,重构了人际传播渠道.随着党员中网民."数字原住民"比重持续攀升,党员工作生活信息化.网络化.数据化持续加深,传统的党建方式对党员,特别是年轻一 ...

  6. 记录--vue中封装一个右键菜单组件(复制粘贴即可使用)

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 组件介绍 关于web端的右键功能常用的地方有表格的右键,或者tab标签的右键等,本文记录一下封装一个右键菜单组件的思路步骤代码. 程序员除 ...

  7. 记录--对于$off,Exclude 和 Extract的一点理解

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.typescript 高阶类型 Exclude 和 Extract Exclude<T, U> TypeScript 2. ...

  8. vue中elementui组件el-dialog拖拽(已处理边界情况)

    全局注册 Vue.directive("elDialogDrag", (el) => { const header = el.querySelector(".el- ...

  9. linux查看资源使用情况

    linux查看资源使用情况 top -c # 查看资源使用情况 top 输出如下内容 top - 14:54:21 up 95 days, 20:03, 3 users, load average: ...

  10. 2024-03-27:用go语言,多维费用背包。 给你一个二进制字符串数组 strs 和两个整数 m 和 n, 请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个

    2024-03-27:用go语言,多维费用背包. 给你一个二进制字符串数组 strs 和两个整数 m 和 n, 请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 ...