.NET 6 Web API 如何获取并验证客户端证书
结论
Request.HttpContext.Connection.ClientCertificate 为 null,通常不是读取代码出了问题,而是 TLS 连接中没有客户端证书。
要获取证书,以下条件缺一不可:
- Webhook 使用 HTTPS。
- ASP.NET Core/Kestrel 已配置为请求或强制要求客户端证书。
- 调用方在 TLS 握手时发送客户端证书。
- 如果请求经过 IIS、Nginx、负载均衡器或云网关,需要正确转发代理接收到的证书。
- Postman 必须配置包含私钥的客户端证书。仅发送一份”已知有效”的 JSON 数据并不会产生客户端证书。
X_Client-Cert 不是浏览器、Postman 或 ASP.NET Core 自动生成的标准请求头,所以正常情况下读取结果也是 null。
使用以下方案前,还要确认 Starling 的文档明确说明:调用 Webhook 时,它会将所提供的证书作为 TLS 客户端证书发送。如果文档所说的是”请求体签名”或其他公钥验签机制,就应该验证签名,而不是读取 ClientCertificate。
在 Kestrel 中要求并验证客户端证书
可以在 Program.cs 中配置 Kestrel,让它在 TLS 握手阶段验证 Starling 提供的证书。
下面的示例固定了证书的 SHA-256 指纹,只有与本地 .crt 完全相同的证书才能通过验证:
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Https;
var builder = WebApplication.CreateBuilder(args);
var certificatePath = builder.Configuration["Starling:ClientCertificatePath"]
?? throw new InvalidOperationException(
"Missing Starling:ClientCertificatePath configuration.");
using var pinnedCertificate = new X509Certificate2(certificatePath);
var pinnedCertificateHash = SHA256.HashData(pinnedCertificate.RawData);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.ClientCertificateMode =
ClientCertificateMode.RequireCertificate;
httpsOptions.ClientCertificateValidation =
(certificate, chain, sslPolicyErrors) =>
{
if (certificate is null)
{
return false;
}
var receivedCertificateHash =
SHA256.HashData(certificate.RawData);
return CryptographicOperations.FixedTimeEquals(
receivedCertificateHash,
pinnedCertificateHash);
};
});
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
配置文件可以这样写:
{
"Starling": {
"ClientCertificatePath": "D:\\Certificate\\Starling\\starling-sandbox-api-certificate.crt"
}
}
在生产环境中,最好使用部署环境提供的安全配置或证书存储,不要把本机绝对路径硬编码到程序里。
如果 .crt 使用 PEM 格式,而普通构造函数无法加载,可以改用:
using var pinnedCertificate =
X509Certificate2.CreateFromPemFile(certificatePath);
配置 RequireCertificate 后,未发送证书或证书验证失败的连接会在 TLS 阶段被拒绝,请求通常不会进入 Controller。
在 Controller 中读取证书
完成 TLS 配置后,可以这样读取客户端证书:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/webhook")]
public class WebhookController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Receive()
{
var clientCertificate =
await HttpContext.Connection.GetClientCertificateAsync();
if (clientCertificate is null)
{
return Forbid();
}
// TLS 阶段已经完成证书验证。
// 在这里继续验证并处理 Webhook 数据。
return Ok(new { status = "ok" });
}
}
GetClientCertificateAsync() 是更通用的读取方式,但它无法凭空取得证书。服务器必须在 TLS 握手时请求证书,客户端也必须真正发送证书。
如果 Kestrel 已经验证过证书,通常不必在每个 Controller 中重复比较。
如果要固定公钥而不是整个证书
固定整个证书后,Starling 一旦更换证书,即使新证书使用相同的公钥,验证也会失败。如果对方明确要求 public key pinning,可以比较 Subject Public Key Info:
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
static byte[] GetPublicKeyPin(X509Certificate2 certificate)
{
byte[] subjectPublicKeyInfo =
certificate.PublicKey.ExportSubjectPublicKeyInfo();
return SHA256.HashData(subjectPublicKeyInfo);
}
启动时计算本地证书的公钥摘要:
using var pinnedCertificate = new X509Certificate2(certificatePath);
var pinnedPublicKeyHash = GetPublicKeyPin(pinnedCertificate);
然后验证收到的证书:
httpsOptions.ClientCertificateValidation =
(certificate, chain, sslPolicyErrors) =>
{
if (certificate is null)
{
return false;
}
var receivedPublicKeyHash = GetPublicKeyPin(certificate);
return CryptographicOperations.FixedTimeEquals(
receivedPublicKeyHash,
pinnedPublicKeyHash);
};
固定”完整证书”和固定”公钥”是两种不同的验证方式,具体采用哪一种,必须以 Starling 当前的文档为准。
为什么使用 Postman 时始终为 null
Postman 默认不会发送客户端证书。需要在它的客户端证书设置中,针对准确的域名和端口配置:
- 客户端证书,例如
.pfx或.crt; - 与证书匹配的私钥;
- 私钥密码,如果存在;
- 与请求 URL 一致的 host 和 port。
只有 Starling 发布的 .crt 公钥证书还不够。Postman 需要对应的私钥才能证明身份,而 Starling 不会把自己的私钥提供给你,所以不能通过 Postman 冒充 Starling。
调试时可以采用以下方式:
- 让 Starling Sandbox 发出真实的 Webhook;
- 自己生成一套测试客户端证书和私钥,并在开发环境中固定这份测试证书;
- 使用
curl携带自己生成的测试证书:
curl --cert test-client.crt \
--key test-client.key \
--request POST \
--header "Content-Type: application/json" \
--data '{"test":true}' \
https://localhost:5001/api/webhook
使用 IIS、Nginx 或负载均衡器时
如果 HTTPS 在反向代理处终止,Kestrel 看到的是代理新建的连接,而不是 Starling 原始的 TLS 连接。此时,HttpContext.Connection.ClientCertificate 仍可能为 null。
代理层需要完成这些工作:
- 要求客户端提供证书;
- 验证证书;
- 将验证后的证书安全地转发给 ASP.NET Core;
- 删除外部请求中可能伪造的同名 Header;
- 在 ASP.NET Core 中启用证书转发中间件。
ASP.NET Core 的证书转发可以这样配置:
using Microsoft.AspNetCore.HttpOverrides;
using System.Security.Cryptography.X509Certificates;
builder.Services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-Client-Cert";
options.HeaderConverter = headerValue =>
{
byte[] certificateBytes = Convert.FromBase64String(headerValue);
return new X509Certificate2(certificateBytes);
};
});
在读取证书之前启用:
app.UseCertificateForwarding();
Header 名称和编码格式必须与代理实际发送的内容一致,不能自行认定它就是 X_Client-Cert。只有受信任代理写入的证书 Header 才能用于认证,否则客户端可以直接伪造这个 Header。
还需要检查的事项
- 请求 URL 必须使用
https://,不能使用http://。 - Visual Studio 使用 IIS Express 时,TLS 可能由 IIS Express 处理,因此 Kestrel 配置未必生效。调试时要确认当前启动的是项目本身还是 IIS Express。
- Starling Sandbox 和生产环境可能使用不同的证书,需要分别配置。
- 证书可能会轮换,部署时要预留更新固定证书或公钥的机制。
- 不要用
Ok(new { status = "error" })表示认证失败。认证失败应该返回401 Unauthorized、403 Forbidden,或者直接在 TLS 握手阶段拒绝连接。 - 验证来源后,仍要处理 Webhook 重放、重复投递、请求完整性和幂等问题。客户端证书只能证明 TLS 对端持有对应私钥,不能替代其他业务层安全检查。
备注:内容仅供参考。
