SpringAiAlibaba之mitmproxy证书安装与原理解析(九)
前言
在调试 Spring AI 调用 DashScope API 的过程中,我遇到了一个典型的 HTTPS 抓包问题。本文将详细记录如何解决证书验证错误,并深入解析 mitmproxy 如何实现 HTTPS 流量拦截的原理。
问题背景
初始错误
在尝试使用 mitmproxy 抓取 Java 应用的 HTTPS 请求时,遇到了以下错误:
Caused by: sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:148)
at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:129)
at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297)
at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434)
测试场景
- 测试类 :
EnableThinkingExtraBodyTest - 测试方法 :
testEnableThinkingWithExtraBody_UserScenario() - 目标 API :
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions - 框架: Spring AI 1.1.2
- Java 版本: OpenJDK 17 (Homebrew)
- 抓包工具: mitmproxy/mitmweb
配置情况
已经在 IDEA 的 VM options 中配置了代理:
-Dhttp.proxyHost=127.0.0.1
-Dhttp.proxyPort=8080
-Dhttps.proxyHost=127.0.0.1
-Dhttps.proxyPort=8080
但仍然报证书错误。
问题根源分析
为什么会出现证书错误?
1. HTTPS 的证书验证机制
正常的 HTTPS 请求流程:
┌─────────────┐ ┌──────────────┐
│ Java App │ ────HTTPS───────> │ DashScope API│
│ │ <───SSL/TLS──── │ Server │
└─────────────┘ └──────────────┘
│
└── 验证服务器证书
├── 证书由受信任的 CA 签发?✓
├── 证书域名匹配?✓
└── 证书未过期?✓
服务器的证书由**受信任的证书颁发机构(CA)**签发,Java 的 cacerts 信任库中包含这些 CA 的根证书,因此可以验证通过。
2. 加入代理后的请求流程
使用 mitmproxy 作为代理后:
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Java App │ ─HTTPS─>│ mitmproxy │ ─HTTPS─>│ DashScope API│
│ │ <─SSL─ │ (Proxy) │ <─SSL─ │ Server │
└─────────────┘ └──────────────┘ └──────────────┘
│ │
│ └── mitmproxy 的自签名证书
│
└── ❌ 验证失败!
└── mitmproxy 的证书不在信任库中
关键点:
- mitmproxy 充当中间人(Man-in-the-Middle)
- Java 应用连接的是 mitmproxy,而不是真实的服务器
- mitmproxy 出示的是自己签发的证书,不是 DashScope 的证书
- Java 的
cacerts信任库中没有 mitmproxy 的 CA 证书 - 因此证书验证失败
3. 为什么代码中禁用 SSL 验证无效?
尝试在代码中通过 HttpsURLConnection.setDefaultSSLSocketFactory() 禁用 SSL 验证:
java
private static void disableSSLVerification() {
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
为什么无效?
Spring AI 1.1.2 使用的是 Spring RestClient (Spring 6.1+ 的新 HTTP 客户端),它在创建时会独立初始化自己的 SSL 配置 ,不使用全局的 HttpsURLConnection 设置:
java
// OpenAiApi 内部(简化)
this.restClient = RestClient.builder()
.baseUrl(baseUrl)
.requestFactory(createRequestFactory()) // 创建独立的请求工厂
.build();
每个 RequestFactory 有自己的 SSL 上下文,不受全局设置影响。
解决方案:安装 mitmproxy 证书
操作步骤
1. 找到 mitmproxy 证书
mitmproxy 首次运行时会生成自签名的 CA 证书:
bash
ls ~/.mitmproxy/
# 输出:
# mitmproxy-ca-cert.pem <- 这是我们需要的
# mitmproxy-ca-cert.cer
# mitmproxy-ca.pem
2. 定位 Java 的 cacerts 信任库
对于 Homebrew 安装的 OpenJDK 17:
bash
# 查找 OpenJDK 17 路径
ls -la /opt/homebrew/opt/openjdk@17
# cacerts 位置
/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/lib/security/cacerts
3. 导入证书到 Java 信任库
bash
sudo /opt/homebrew/opt/openjdk@17/bin/keytool -importcert \
-file ~/.mitmproxy/mitmproxy-ca-cert.pem \
-alias mitmproxy \
-keystore /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/lib/security/cacerts \
-storepass changeit \
-noprompt
参数说明:
-importcert: 导入证书命令-file: mitmproxy 的 CA 证书路径-alias mitmproxy: 证书别名(便于后续管理)-keystore: Java 信任库路径-storepass changeit: 默认密码(Java cacerts 的默认密码)-noprompt: 不提示确认
4. 验证证书是否安装成功
bash
/opt/homebrew/opt/openjdk@17/bin/keytool -list \
-keystore /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/lib/security/cacerts \
-storepass changeit | grep mitmproxy
输出:
mitmproxy, 2026年1月14日, trustedCertEntry,
为什么安装证书后问题解决了?
安装证书后的验证流程:
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Java App │ ─HTTPS─>│ mitmproxy │ ─HTTPS─>│ DashScope API│
│ │ <─SSL─ │ (Proxy) │ <─SSL─ │ Server │
└─────────────┘ └──────────────┘ └──────────────┘
│ │
└── ✅ 验证通过!
├── mitmproxy 证书由 mitmproxy CA 签发
├── mitmproxy CA 根证书在 cacerts 中
└── 信任链完整
信任链建立:
DashScope Server 的真实证书
↓
mitmproxy 验证
↓
mitmproxy 重新签发证书(使用自己的 CA)
↓
Java App 收到
↓
Java 验证证书 → 发现是 mitmproxy CA 签发
↓
在 cacerts 中找到 mitmproxy CA 根证书
↓
验证通过 ✅
mitmproxy 工作原理深度解析
HTTPS 中间人攻击的合法应用
mitmproxy 实际上是在执行可控的中间人攻击,但用于合法的调试目的。
完整的请求路径分析
阶段 1: 建立到 mitmproxy 的连接
1. Java App 发起 HTTPS 请求
↓
2. 通过代理配置连接到 127.0.0.1:8080
↓
3. 发送 CONNECT 请求:
CONNECT dashscope.aliyuncs.com:443 HTTP/1.1
Host: dashscope.aliyuncs.com:443
↓
4. mitmproxy 响应:
HTTP/1.1 200 Connection established
阶段 2: TLS 握手(Java App ↔ mitmproxy)
5. Java App 发起 TLS ClientHello
↓
6. mitmproxy 响应 ServerHello + 证书
证书内容:
- 主题: CN=dashscope.aliyuncs.com
- 签发者: CN=mitmproxy ← 注意:是 mitmproxy 签发的!
- 有效期: mitmproxy 动态生成
↓
7. Java App 验证证书
├── 检查签发者: mitmproxy CA
├── 在 cacerts 中查找 mitmproxy CA 根证书 ✓
├── 验证证书链 ✓
└── 验证域名匹配 ✓
↓
8. TLS 握手完成
建立加密连接(使用对称密钥)
阶段 3: mitmproxy 转发到真实服务器
9. mitmproxy 解密收到的请求
(因为 mitmproxy 拥有私钥)
↓
10. mitmproxy 发起到真实服务器的 TLS 连接
┌────────────────────────────────┐
│ mitmproxy ─TLS─> DashScope API │
└────────────────────────────────┘
- 验证 DashScope 的真实证书
- 建立独立的加密连接
↓
11. mitmproxy 转发原始请求:
POST /compatible-mode/v1/chat/completions HTTP/1.1
Authorization: Bearer sk-xxx
Content-Type: application/json
{
"model": "qwen-plus",
"messages": [...],
"enable_thinking": true
}
阶段 4: 响应返回
12. DashScope 返回响应
↓
13. mitmproxy 收到响应(明文,因为已解密)
↓
14. mitmproxy 记录/展示请求和响应(mitmweb 界面)
↓
15. mitmproxy 加密响应(使用与 Java App 的密钥)
↓
16. 发送给 Java App
↓
17. Java App 解密响应(使用自己的密钥)
↓
18. 应用程序收到响应
关键技术点
1. 动态证书生成
mitmproxy 为每个 HTTPS 域名动态生成证书:
python
# mitmproxy 内部伪代码
def generate_cert(hostname):
cert = Certificate()
cert.subject = f"CN={hostname}" # 匹配目标域名
cert.issuer = "CN=mitmproxy" # 由 mitmproxy CA 签发
cert.sign_with(mitmproxy_ca_key) # 使用 CA 私钥签名
return cert
这样可以确保:
- 证书的域名与目标服务器匹配(通过 SNI 检查)
- 证书由 mitmproxy CA 签发(可以验证)
- 每个域名都有独立的证书
2. 双向加密连接
mitmproxy 同时维护两个独立的 TLS 连接:
Java App ←─── TLS 连接 1 ───→ mitmproxy ←─── TLS 连接 2 ───→ DashScope
(使用 mitmproxy 证书) (使用 DashScope 真实证书)
- 连接 1: 使用 mitmproxy 的自签名证书,Java App 需要信任 mitmproxy CA
- 连接 2: 使用 DashScope 的真实证书,mitmproxy 验证真实证书
3. 流量解密与记录
因为 mitmproxy 拥有连接 1 的私钥,可以解密所有流量:
加密的请求 → mitmproxy 解密 → 明文请求(可查看、修改)
↓
记录到日志
↓
明文响应 ← mitmproxy 解密 ← 加密的响应
mitmweb 界面展示
在浏览器访问 http://localhost:8081,可以看到:
┌──────────────────────────────────────────────────────────┐
│ mitmweb - HTTP(S) Traffic Inspector │
├──────────────────────────────────────────────────────────┤
│ POST https://dashscope.aliyuncs.com/compatible-mode/... │
│ ├─ Request │
│ │ ├─ Headers │
│ │ │ ├─ Authorization: Bearer sk-xxx │
│ │ │ └─ Content-Type: application/json │
│ │ └─ Body (JSON) │
│ │ { │
│ │ "model": "qwen-plus", │
│ │ "messages": [...], │
│ │ "enable_thinking": true ⬅️ 可以清楚看到 │
│ │ } │
│ └─ Response │
│ └─ Body (JSON) │
│ { │
│ "choices": [...], │
│ "usage": {...} │
│ } │
└──────────────────────────────────────────────────────────┘
实际应用:验证 extraBody 问题
测试目的
验证 Spring AI 1.1.2 的 OpenAiChatOptions.extraBody() 是否将 enable_thinking 参数正确传递到 HTTP 请求中。
测试代码
java
@Test
void testEnableThinkingWithExtraBody_UserScenario() {
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model("qwen-plus")
.extraBody(Map.of("enable_thinking", true)) // 期望传递这个参数
.streamUsage(false)
.build();
OpenAiApi openAiApi = OpenAiApi.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.completionsPath("/chat/completions")
.apiKey(apiKey)
.build();
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(options)
.build();
ChatResponse response = chatModel.call(
new Prompt(new UserMessage("请分析一下1+1等于几?"))
);
}
抓包结果
通过 mitmproxy 查看实际的 HTTP 请求体:
json
{
"model": "qwen-plus",
"messages": [
{
"role": "user",
"content": "请分析一下1+1等于几?"
}
],
"stream": false,
"streamUsage": false
// ❌ 注意:没有 "enable_thinking" 字段!
}
结论:
- ✅
extraBody在OpenAiChatOptions对象中正确配置 - ❌ 但
extraBody的内容没有出现在实际的 HTTP 请求 JSON 中 - ❌ 这证实了 Spring AI 1.1.2 的
OpenAiApi存在 bug(issue #3990)
安全注意事项
为什么要安装证书?
- 遵循安全最佳实践:即使是调试,也应该使用正确的证书验证
- 避免全局禁用 SSL:禁用 SSL 会影响所有 HTTPS 连接,存在安全风险
- 证书可控:只信任 mitmproxy CA,不影响其他证书验证
生产环境警告
⚠️ 永远不要在生产环境中:
- 安装 mitmproxy 证书
- 禁用 SSL 证书验证
- 使用中间人代理
这些操作仅用于开发和调试!
卸载证书
调试完成后,可以删除 mitmproxy 证书:
bash
sudo /opt/homebrew/opt/openjdk@17/bin/keytool -delete \
-alias mitmproxy \
-keystore /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/lib/security/cacerts \
-storepass changeit
总结
问题回顾
- 原始问题:使用 mitmproxy 抓包时报证书验证错误
- 根本原因:Java 不信任 mitmproxy 的自签名 CA 证书
- 失败尝试:代码中禁用 SSL 验证对 Spring RestClient 无效
- 正确方案:将 mitmproxy CA 证书安装到 Java cacerts 信任库
技术要点
- HTTPS 验证机制:客户端必须信任服务器证书的签发者
- 中间人代理原理:mitmproxy 通过动态生成证书实现流量拦截
- 双向加密连接:客户端 ↔ mitmproxy 和 mitmproxy ↔ 服务器是两个独立的 TLS 连接
- 证书信任链:Java 通过 cacerts 信任库验证证书链的完整性
完整请求路径
Java 应用
↓ (1) 配置代理,连接 mitmproxy
mitmproxy (127.0.0.1:8080)
↓ (2) TLS 握手,出示动态生成的证书
Java 验证证书
↓ (3) 在 cacerts 中找到 mitmproxy CA,验证通过
建立加密连接 1
↓ (4) 发送加密的 HTTP 请求
mitmproxy 解密请求
↓ (5) 记录明文请求内容(可在 mitmweb 查看)
mitmproxy 建立到真实服务器的连接
↓ (6) TLS 握手,验证服务器真实证书
建立加密连接 2
↓ (7) 转发请求到真实服务器
真实服务器 (dashscope.aliyuncs.com)
↓ (8) 处理请求,返回响应
mitmproxy 收到响应
↓ (9) 解密并记录响应内容
↓ (10) 加密后转发给 Java 应用
Java 应用收到响应
↓ (11) 解密并处理
应用程序完成请求
实用价值
通过这个过程,我们:
- ✅ 成功配置了 HTTPS 抓包环境
- ✅ 理解了 HTTPS 证书验证机制
- ✅ 掌握了 mitmproxy 的工作原理
- ✅ 验证了 Spring AI 的 extraBody bug
- ✅ 为调试 API 集成问题提供了强大工具
参考资料
作者注 :本文基于实际调试 Spring AI 与 DashScope API 集成时遇到的问题编写,所有命令和代码均已验证可用。
欢迎关注,一起学习,一起进步~