1. 项目概述:当Spring AI遇上中间人代理

最近在折腾一个基于Spring AI Alibaba的项目,需要让AI应用去调用一些外部API。但在调试过程中,我发现一个棘手的问题:某些目标服务的HTTPS流量无法被常规的抓包工具(如Charles、Fiddler)正常解密和观察。这对于排查请求参数、响应格式乃至理解AI模型与外部服务的交互细节来说,几乎是“两眼一抹黑”。相信很多做接口调试、逆向分析或者安全测试的朋友都遇到过类似场景——你需要窥探或修改经过SSL/TLS加密的流量。

这时, mitmproxy 就登场了。它不仅仅是一个抓包工具,更是一个强大的中间人代理,专门用于拦截、检查、修改和重放HTTP/HTTPS流量。而要让 mitmproxy 成功解密HTTPS流量,核心的一步就是在客户端(也就是我们的Spring AI应用,或者更底层地,是JVM或操作系统)安装并信任 mitmproxy 生成的CA(证书颁发机构)证书。这个过程看似简单,但在不同的运行环境(比如Docker容器、特定的JDK版本、或使用了严格安全策略的服务器)下,往往会踩到各种各样的坑。本文,我就结合在Spring AI Alibaba项目中集成的实战经历,从头到尾拆解 mitmproxy 证书安装的步骤、背后的原理,以及那些官方文档不会告诉你的避坑指南。

2. 核心原理:HTTPS中间人攻击(MitM)的“合法”应用

在深入实操之前,我们必须先搞明白 mitmproxy 到底是怎么工作的。这不仅仅是点一下“安装证书”按钮那么简单,理解原理能让你在遇到问题时,知道该从哪个方向去排查。

2.1 TLS/SSL握手与证书信任链

HTTPS的安全基石是TLS/SSL协议,其核心在于“信任”。当客户端(如浏览器、你的Java应用)访问一个HTTPS网站(如 https://api.example.com )时,会发生一次握手过程:

  1. 服务端发送证书 :服务器将其公钥证书发送给客户端。
  2. 客户端验证证书 :客户端会做以下几件事:
    • 检查证书是否由可信的CA签发 :客户端操作系统或运行环境(如JVM)内置了一个“受信任的根证书存储区”。客户端会验证服务器证书的签发者是否在这个信任列表里,或者其签发链的顶端根证书是否在信任列表里。
    • 检查证书域名是否匹配 :验证证书中的 Common Name (CN) Subject Alternative Name (SAN) 是否与正在访问的域名一致。
    • 检查证书是否过期
  3. 协商密钥 :如果验证通过,客户端会使用服务器的公钥加密一个“预主密钥”并发送给服务器,双方据此生成相同的会话密钥,用于后续通信的对称加密。

mitmproxy 正是利用了这个信任机制。它扮演了一个“中间人”的角色:

  1. 拦截连接 :当客户端配置代理指向 mitmproxy 后,所有流量先到达 mitmproxy
  2. 动态签发证书 :对于HTTPS请求, mitmproxy 会即时伪造一个与目标服务器域名相同的证书。这个伪造证书的签发者,不是DigiCert、Let‘s Encrypt这些公认的CA,而是 mitmproxy 自己生成的 根CA证书
  3. 双重握手 mitmproxy 分别与客户端和真实服务器建立TLS连接。与客户端连接时,它使用自己伪造的证书;与服务器连接时,它像正常客户端一样进行验证。
  4. 解密与转发 :因为 mitmproxy 拥有自己伪造证书对应的私钥,它可以解密客户端发送来的数据,然后重新加密发给真实服务器,反之亦然。这样,它就能看到明文的流量。

2.2 为什么必须安装mitmproxy的CA证书?

关键点就在这里:默认情况下,客户端(你的Spring AI应用)不信任 mitmproxy 这个“自封的CA”。当它收到 mitmproxy 伪造的 api.example.com 证书时,去验证签发者,发现签发者 mitmproxy 不在自己信任的根证书列表里,于是会立即报错,通常是 SSLHandshakeException CertificateException ,连接中断。

安装 mitmproxy 的CA证书,本质就是将 mitmproxy 的根证书添加到客户端的“受信任的根证书颁发机构”列表中 。这样,客户端再收到由这个根证书签发的任何子证书(比如伪造的 api.example.com 证书),都会认为它是可信的,握手成功, mitmproxy 才能顺利解密流量。

对于我们的Spring AI应用来说,这个“客户端”的信任库可能存在于两个层面:

  • 操作系统级 :将证书安装到操作系统(如Windows、macOS、Linux)的全局信任库。所有运行在该系统上的程序(包括Java应用)默认都会信任。
  • JVM级 :将证书导入到Java运行时环境(JRE/JDK)专用的证书库 cacerts 中。这是更常见、更可控的方式,尤其适用于容器化部署或特定应用。

3. 实操准备:启动mitmproxy与获取证书

理论清晰后,我们开始动手。首先确保你已安装 mitmproxy 。可以通过pip安装: pip install mitmproxy

3.1 启动mitmproxy并生成根证书

第一次运行 mitmproxy 时,它会在用户目录下(如 ~/.mitmproxy/ )自动生成一套私钥和根证书。

# 启动mitmproxy的Web交互界面
mitmweb

或者以守护进程模式启动:

mitmdump -s your_script.py # 如果需要自定义脚本

启动后,关键的一步是找到并导出其CA证书。证书通常位于:

  • 默认位置 ~/.mitmproxy/mitmproxy-ca-cert.pem (PEM格式) 或 ~/.mitmproxy/mitmproxy-ca-cert.cer (DER格式)。

如果找不到,可以通过 mitmproxy 的Web界面(默认 http://127.0.0.1:8081 )访问,在**“证书”** 选项卡中直接下载。

注意 mitmproxy 的CA证书是 长期有效 的(通常10年)。这意味着一旦安装并信任,它在很长时间内都能用于解密你的流量。务必妥善保管私钥(在 ~/.mitmproxy 目录下),不要泄露。

3.2 证书格式辨析:PEM vs DER vs PKCS#12

拿到证书文件后,你可能会看到不同后缀,理解它们有助于后续操作:

  • PEM (.pem, .crt, .cer) : 最常见的格式,文本格式,以 -----BEGIN CERTIFICATE----- 开头。适用于大多数场景,包括导入Java cacerts
  • DER (.der, .cer) : 二进制格式。某些Windows系统导入时需要此格式。
  • PKCS#12 (.p12, .pfx) : 一种归档文件格式,可以包含证书、私钥以及整个证书链。通常用于备份或迁移证书。

对于JVM导入,我们主要使用 PEM 格式。

4. 核心环节:为Spring AI应用安装证书

Spring AI应用本质上是一个Java应用,其SSL/TLS行为由JVM管理。因此,我们的核心任务是将 mitmproxy 的CA证书导入到JVM使用的信任库中。这里提供两种主流方法。

4.1 方法一:导入到JVM全局信任库(cacerts)

这是最彻底的方法,修改后,所有在该JVM上运行的应用都会信任 mitmproxy

步骤:

  1. 定位JRE证书库 :找到你的JDK/JRE安装目录下的 lib/security/cacerts 文件。例如:

    • /usr/lib/jvm/java-11-openjdk-amd64/lib/security/cacerts
    • C:\Program Files\Java\jdk-17\lib\security\cacerts
  2. 使用keytool导入证书 keytool 是JDK自带的密钥和证书管理工具。

    keytool -import -alias mitmproxy -keystore /path/to/your/jre/lib/security/cacerts -file ~/.mitmproxy/mitmproxy-ca-cert.pem -storepass changeit
    
    • -alias mitmproxy : 为证书在库中起一个别名,方便管理。
    • -keystore : 指定 cacerts 文件的路径。
    • -file : 指定要导入的PEM格式证书文件路径。
    • -storepass changeit : cacerts 的默认密码是 changeit 。如果修改过,请使用修改后的密码。
  3. 确认安装 :命令行会提示你确认证书指纹,并询问是否信任此证书,输入 yes 即可。

实操心得

  • 权限问题 :在Linux/macOS上,修改 cacerts 可能需要 sudo 权限。
  • 多JDK版本 :确保你导入证书的 cacerts 文件,正是你的Spring AI应用运行时使用的那个JRE。在IDE中运行和用 java -jar 运行可能使用的JRE路径不同,可以通过 System.getProperty("java.home") 在代码中打印确认。
  • 容器化部署 :如果你在Docker容器中运行应用,需要在构建Docker镜像时执行导入操作。通常是在Dockerfile中 COPY 证书文件,然后运行 keytool -import 命令。

4.2 方法二:在应用中指定自定义信任库(推荐)

修改全局 cacerts 影响范围大,有时我们只想让特定应用信任 mitmproxy 。这时可以创建一个独立的信任库文件,并在启动应用时指定它。

步骤:

  1. 创建新的信任库文件

    # 创建一个新的JKS格式的信任库,并导入mitmproxy证书
    keytool -import -alias mitmproxy -keystore /path/to/my-mitm-truststore.jks -file ~/.mitmproxy/mitmproxy-ca-cert.pem -storepass mypassword -noprompt
    
    • -noprompt : 非交互模式,直接信任导入的证书。
  2. 在Spring Boot应用中配置 :通过JVM参数指定使用这个自定义信任库。

    • 方式A:启动参数 (适用于 java -jar 或IDE配置)
      -Djavax.net.ssl.trustStore=/path/to/my-mitm-truststore.jks
      -Djavax.net.ssl.trustStorePassword=mypassword
      
    • 方式B:在 application.properties application.yml 中配置 (Spring Boot)
      # application.properties
      server.ssl.trust-store=/path/to/my-mitm-truststore.jks
      server.ssl.trust-store-password=mypassword
      # 如果你的应用是客户端,需要调用外部HTTPS服务,还需要配置RestTemplate或WebClient
      
      对于使用Spring AI Alibaba的 ChatClient 等组件,它们底层通常使用 RestTemplate WebClient ,你需要为这些HTTP客户端配置自定义的 SSLContext ,加载你的自定义信任库。这涉及到一些额外的Bean配置。

注意事项 : 方法二更安全、更清晰,但配置稍复杂。特别是当应用使用多种HTTP客户端时,需要确保每个客户端都正确配置了SSL上下文。对于Spring AI Alibaba,如果你发现配置了JVM参数仍不生效,很可能是因为其内部使用的HTTP客户端(如Apache HttpClient、OkHttp)有自己的SSL配置逻辑,需要深入研究其文档或源码进行配置。

5. 配置Spring AI Alibaba应用使用代理

证书安装好后,还需要让Spring AI应用知道流量要走 mitmproxy

5.1 设置JVM全局代理

最简单的方式是通过系统属性设置全局代理,这会影响所有基于 java.net.URLConnection 的HTTP调用(很多HTTP库会遵循这个设置)。

java -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=8080 \
     -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=8080 \
     -Djavax.net.ssl.trustStore=/path/to/my-mitm-truststore.jks \
     -Djavax.net.ssl.trustStorePassword=mypassword \
     -jar your-spring-ai-app.jar

5.2 在HTTP客户端中配置代理

如果全局代理不生效(例如,应用使用了不遵循系统属性的HTTP客户端,如OkHttp或Apache HttpClient的高级配置),则需要在创建HTTP客户端时显式配置代理。

以在Spring配置中自定义一个 RestTemplate 为例:

@Configuration
public class ProxyConfig {

    @Bean
    public RestTemplate restTemplate() throws Exception {
        // 1. 加载自定义信任库(包含mitmproxy CA证书)
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        try (InputStream is = new FileInputStream("/path/to/my-mitm-truststore.jks")) {
            trustStore.load(is, "mypassword".toCharArray());
        }

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), null);

        // 2. 创建使用代理和自定义SSL上下文的HttpClient
        HttpClient httpClient = HttpClientBuilder.create()
                .setSSLContext(sslContext)
                .setProxy(new HttpHost("127.0.0.1", 8080)) // mitmproxy地址
                .build();

        // 3. 使用这个HttpClient创建RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
        return new RestTemplate(factory);
    }
}

然后,你需要确保Spring AI Alibaba的客户端(如 AlibabaDashScopeChatClient )使用的是你自定义的这个 RestTemplate 。这可能需要通过其提供的配置类或自定义Bean来实现,具体取决于Spring AI Alibaba的版本和设计。

6. 常见问题与排查技巧实录

即使按照步骤操作,你也可能遇到问题。下面是我踩过的一些坑和解决方案。

6.1 证书已安装,但SSL握手仍然失败

  • 症状 javax.net.ssl.SSLHandshakeException: PKIX path building failed ... unable to find valid certification path to requested target
  • 排查思路
    1. 确认证书确实导入成功 :使用命令检查 cacerts 或自定义信任库。
      keytool -list -keystore /path/to/truststore.jks -storepass yourpassword | grep mitmproxy
      
    2. 确认应用使用了正确的信任库 :在应用启动时添加 -Djavax.net.debug=ssl:handshake 参数,会打印详细的SSL握手日志。在日志中搜索 trustStore ,看它加载的是哪个文件。
    3. 检查证书别名冲突 :如果 cacerts 中已有同名( alias )证书,新证书可能未覆盖旧证书。尝试删除旧别名再导入,或使用一个独特的别名。
    4. HTTP客户端库绕过JVM默认信任库 :一些HTTP客户端(如OkHttp、Netty)可能默认使用它们自己维护的证书列表,或者需要显式配置信任管理器。确保你为这些客户端配置了正确的 SSLContext

6.2 代理设置不生效,流量未经过mitmproxy

  • 症状 mitmproxy 界面没有收到任何请求,应用直接连到了目标服务器。
  • 排查思路
    1. 验证代理连通性 :先用一个简单的命令测试代理是否工作,例如 curl -x http://127.0.0.1:8080 https://httpbin.org/ip ,看看 mitmproxy 能否捕获到。
    2. 检查应用使用的HTTP客户端 :Spring AI Alibaba可能使用了异步的 WebClient 或其它库。全局的 -Dhttps.proxyHost 参数对 WebClient 默认不生效。你需要在构建 WebClient 时通过 .proxy() 方法设置代理。
    3. 检查是否被环境变量覆盖 :有些运行环境(如Kubernetes)或代码中可能会设置 NO_PROXY 环境变量,导致对某些地址跳过代理。

6.3 在Docker容器中运行应用的证书问题

  • 场景 :应用运行在Docker容器内, mitmproxy 运行在宿主机。
  • 解决方案
    1. 构建时导入证书 :在Dockerfile中将 mitmproxy 的CA证书复制到容器内,并导入到容器JVM的 cacerts 中。
      FROM openjdk:11-jre-slim
      COPY mitmproxy-ca-cert.pem /tmp/
      RUN keytool -import -alias mitmproxy -keystore $JAVA_HOME/lib/security/cacerts \
          -file /tmp/mitmproxy-ca-cert.pem -storepass changeit -noprompt
      COPY your-app.jar /app.jar
      CMD ["java", "-jar", "/app.jar"]
      
    2. 运行时挂载证书 :将宿主机上已包含 mitmproxy 证书的自定义信任库文件,作为卷挂载到容器内指定路径,然后通过JVM参数 -Djavax.net.ssl.trustStore 指向这个挂载路径。
    3. 代理地址 :在容器内,宿主机地址不是 127.0.0.1 。可以使用特殊的DNS名称 host.docker.internal (Docker Desktop for Mac/Windows)或 172.17.0.1 (默认Docker网桥网关)来指向宿主机。代理设置应类似 -Dhttps.proxyHost=host.docker.internal

6.4 特定域名或API仍然无法解密

  • 现象 :大部分HTTPS流量可以解密,但个别API(尤其是移动端或SDK专用的API)在 mitmproxy 中显示为 TLS握手失败 Client Hello记录
  • 可能原因及解决
    • 证书钉扎(Certificate Pinning) :应用在代码中硬编码了服务端证书的公钥或指纹,只信任特定的证书,直接忽略系统信任库。这是安全应用对抗MitM的常用手段。对于这种情况, mitmproxy 无法解密。解决方案通常需要反编译应用并修改代码,难度极大,且可能涉及法律风险, 仅供安全研究在合法授权范围内进行
    • HTTP/2或ALPN问题 :尝试在 mitmproxy 启动时添加 --set http2=false 参数,强制使用HTTP/1.1。
    • SNI(服务器名称指示)问题 :较新版本的 mitmproxy 对SNI处理很好,如果遇到问题,可以检查目标服务器是否对SNI有特殊要求。

7. 安全与伦理考量

使用 mitmproxy 解密HTTPS流量是一项强大的技术,但必须负责任地使用。

  1. 仅用于合法目的 :只对你拥有或有权测试的系统、以及你自己开发的应用程序进行调试和分析。未经授权拦截他人流量是非法行为。
  2. 保护你的CA证书 mitmproxy 的根证书私钥是解密所有流量的万能钥匙。一旦泄露,攻击者可以在你信任此证书的设备上实施中间人攻击。测试结束后,可以考虑从信任库中移除该证书。
  3. 区分环境 :永远不要将安装了 mitmproxy CA证书的设备或配置用于生产环境或处理敏感个人数据的日常浏览。建议使用独立的虚拟机、容器或测试设备来进行这类调试工作。
  4. 理解局限性 :正如前文所述,证书钉扎等技术可以防御此类代理工具。这提醒我们,在设计和开发自己的应用时,也应考虑采用类似的安全措施来保护用户数据。

通过以上步骤和原理的解析,你应该能够成功地在Spring AI Alibaba项目中配置 mitmproxy ,并清晰地洞察其与外部服务的每一次HTTPS对话。这个过程不仅是解决问题的技巧,更是一次对TLS/SSL协议、Java安全体系以及网络调试工具的深入理解。在实际操作中,耐心查看日志、逐步验证每一步是成功的关键。

更多推荐