.NET MAUI 在 Android 模拟器中调用 API 时出现 HttpRequestException
这段异常表明连接在 TLS 握手阶段失败,但不能据此确定原因是自签名证书不受信任。先确认请求地址和 API 实际返回的证书,再检查证书信任和名称是否匹配。新建或导入证书不一定会改变 API 使用的证书。
为什么现有配置没有解决问题?
cleartextTrafficPermitted="true" 控制明文 HTTP,不能修复 HTTPS 证书校验错误。
<debug-overrides> 为可调试应用增加信任来源。src="user" 表示允许使用 Android 用户证书存储中的信任锚;它不会关闭所有证书检查,也不会自动信任开发计算机上的证书。该配置只在 android:debuggable="true" 时生效。参见 Android 网络安全配置。
下面的命令只是在 Windows 证书存储中创建证书:
$cert = New-SelfSignedCertificate -Subject "CN=DevCertificate" -CertStoreLocation "Cert:\CurrentUser\My"
它没有把证书绑定到 API 的 HTTPS 端口,也不能据此认定该证书可以用于访问 localhost 或 10.0.2.2。
1. 确认模拟器访问的是正确地址和端口
对于使用标准 Android Emulator 网络的模拟器,10.0.2.2 指向开发计算机的回环接口;模拟器内的 localhost 指向模拟器自身。参见 Android Emulator 网络地址说明。
如果开发计算机上的 API 地址是:
https://localhost:<HTTPS_PORT>/
模拟器中通常应使用:
https://10.0.2.2:<HTTPS_PORT>/
将 <HTTPS_PORT> 替换为 API 实际监听的 HTTPS 端口,并使用实际接口路径。检查 API 启动日志和当前启动配置,避免把 HTTP 端口当成 HTTPS 端口。
两个项目位于同一个解决方案中,并不会让模拟器共享 Windows 的网络地址或证书信任设置。
2. 确认 API 实际返回哪张证书
先在开发计算机上访问 API 的 HTTPS 地址,查看服务端证书的 Subject、Issuer、有效期、Subject Alternative Name(SAN)和指纹,再与导出的证书比较。
如果 API 使用 ASP.NET Core,先确认当前使用的是 Kestrel、IIS Express 还是其他宿主,再核对对应的 HTTPS 证书配置。创建新证书后,只有服务端实际加载了它,客户端才会收到它。
还要分别验证:
- 信任关系:Android 是否信任服务端证书或其签发 CA。
- 名称匹配:证书是否适用于请求 URL 中的主机名或 IP 地址。
例如,访问 https://10.0.2.2:... 时,仅适用于 localhost 的证书仍可能出现名称不匹配。需要检查 SAN 是否包含适用的 IP 地址条目,不能只看 CN=DevCertificate。Android 会分别进行证书和主机名校验,相关机制见 Android TLS 安全说明。
3. 需要保留正常校验时,明确配置开发证书信任
可以把开发 CA 的公开证书打包进调试应用。如果服务端直接使用自签名证书,则使用对应的公开证书。无需把服务端私钥放进应用。
例如,将 PEM 格式的公开证书放到:
Platforms/Android/Resources/raw/debug_ca.pem
再配置:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="@raw/debug_ca" />
</trust-anchors>
</debug-overrides>
</network-security-config>
保留现有 Manifest 中的引用,确认 XML 和证书文件作为 AndroidResource 打包,然后重新构建并部署 Debug 应用。Android 支持通过资源文件配置 PEM 或 DER 格式的信任证书,见 自定义 CA 配置说明。
配置好信任来源后,证书仍须与请求地址匹配。将 .pfx 安装到 Android 后,也应检查它是否真的进入了应用使用的信任存储,不能仅凭“安装成功”判断配置有效。
4. 仅限本地调试时,可对指定开发证书设置例外
如果暂时需要使用现有开发证书,可以为实际调用 API 的 HttpClient 配置 ServerCertificateCustomValidationCallback。Microsoft 的 MAUI 本地 HTTPS 调试指南 提供了这种做法。
下面的示例进一步限制了例外范围:仅在 Android 的 Debug 构建中,针对指定地址、端口和证书 SHA-256 指纹设置校验例外。端口 5001 只是示例,必须改成实际值;指纹也必须替换为已经核实的服务端证书指纹,使用不含空格和冒号的十六进制字符串。
using System;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography;
var apiUri = new Uri("https://10.0.2.2:5001/");
var handler = new HttpClientHandler();
#if DEBUG && ANDROID
const string expectedFingerprint =
"REPLACE_WITH_ACTUAL_SHA256_CERTIFICATE_FINGERPRINT";
handler.ServerCertificateCustomValidationCallback =
(request, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
return true;
var uri = request.RequestUri;
if (certificate == null || uri == null)
return false;
return uri.Scheme == Uri.UriSchemeHttps
&& uri.Host == apiUri.Host
&& uri.Port == apiUri.Port
&& string.Equals(
certificate.GetCertHashString(HashAlgorithmName.SHA256),
expectedFingerprint,
StringComparison.OrdinalIgnoreCase);
};
#endif
var client = new HttpClient(handler)
{
BaseAddress = apiUri
};
必须让发出失败请求的代码使用这个 client。如果项目通过依赖注入创建 HttpClient,应在对应的客户端注册中配置 handler;另建一个未被使用的实例不会生效。
这段例外会绕过指定证书的正常校验错误,包括名称或有效期错误,因此只适用于受控的本地调试。证书更新后,需要重新核实并更新指纹。正式环境应使用名称匹配、有效且受信任的证书。
5. 仍然失败时,按错误类型继续排查
查看完整异常链、Android 日志,以及证书回调中的 errors:
RemoteCertificateChainErrors:检查信任锚、证书有效期以及服务端是否发送完整证书链。RemoteCertificateNameMismatch:检查请求 URL 与证书 SAN。- 连接被拒绝或超时:检查 API 是否启动、监听端口和网络可达性。
- 回调完全没有执行:检查实际使用的
HttpClient、handler 和重定向后的请求地址,必要时补充 .NET MAUI 版本与 Android 日志。
现有错误堆栈没有给出具体的证书校验原因。拿到实际请求 URL、服务端证书和底层错误后,再决定采用正常信任配置还是本地调试例外。
备注:内容仅供参考。
