将客户端证书添加到 .NET Core HttpClient
Posted
技术标签:
【中文标题】将客户端证书添加到 .NET Core HttpClient【英文标题】:Add client certificate to .NET Core HttpClient 【发布时间】:2016-10-13 06:44:36 【问题描述】:我正在使用 .NET Core 并构建一个利用支付 API 的 API。有一个客户端证书需要添加到双向 SSL 身份验证的请求中。
如何在 .NET Core 中使用 HttpClient
实现这一点?
我查看了各种文章,发现HttpClientHandler
没有提供任何添加客户端证书的选项。
【问题讨论】:
【参考方案1】:我按照以下步骤为我的平台 (Linux Mint17.3) 运行了全新安装:.NET Tutorial - Hello World in 5 minutes。我创建了一个针对netcoreapp1.0
框架的新控制台应用程序,能够提交客户端证书;但是,我在测试时确实收到了“SSL 连接错误”(CURLE_SSL_CONNECT_ERROR 35
),即使我使用了有效的证书。我的错误可能特定于我的 libcurl。
我在 Windows 7 上运行了完全相同的程序,它完全按照需要运行。
// using System.Net.Http;
// using System.Security.Authentication;
// using System.Security.Cryptography.X509Certificates;
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
var result = client.GetAsync("https://apitest.startssl.com").GetAwaiter().GetResult();
【讨论】:
哇哈,伙计,非常感谢您花时间帮助我。这些天我都在休假。 我很好奇。你是让它在 linux 机器或 windows 上工作吗? 我让它在 Windows (10) 和 Linux(Mint 17.3、18 和 18.1)上运行。当我尝试使用没有私钥的证书时,我确实收到了“SSL 连接错误”(CURLE_SSL_CONNECT_ERROR 35)。 @yfisaqt 你能详细说明你是如何看到 CURL 错误的吗?我发现我可以在 Windows 上使用证书签名,但 Ubuntu 17.04 上的相同代码会导致错误。 @Tom 从技术角度来说,这取决于文件格式。请记住,在我升级到 .NET Core SDK 2 之前已经回答了这个问题,因此这在 2.0 中的行为可能会有所不同。在我回答这个问题时,Windows 要求该文件是 PFX,但在 Linux 上我能够使用常规证书(无私钥)。【参考方案2】:我有一个类似的项目,我通过服务在服务之间以及移动设备和桌面设备之间进行通信。
我们使用 EXE 文件中的 Authenticode 证书来确保执行请求的是我们的二进制文件。
在请求方(帖子过于简化)。
Module m = Assembly.GetEntryAssembly().GetModules()[0];
using (var cert = m.GetSignerCertificate())
using (var cert2 = new X509Certificate2(cert))
var _clientHandler = new HttpClientHandler();
_clientHandler.ClientCertificates.Add(cert2);
_clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
var myModel = new Dictionary<string, string>
"property1","value" ,
"property2","value" ,
;
using (var content = new FormUrlEncodedContent(myModel))
using (var _client = new HttpClient(_clientHandler))
using (HttpResponseMessage response = _client.PostAsync($"url/controler/action", content).Result)
response.EnsureSuccessStatusCode();
string jsonString = response.Content.ReadAsStringAsync().Result;
var myClass = JsonConvert.DeserializeObject<MyClass>(jsonString);
然后我在获取请求的操作上使用以下代码:
X509Certificate2 clientCertInRequest = Request.HttpContext.Connection.ClientCertificate;
if (!clientCertInRequest.Verify() || !AllowedCerialNumbers(clientCertInRequest.SerialNumber))
Response.StatusCode = 404;
return null;
我们宁愿提供 404 而不是 500,因为我们喜欢那些尝试使用 URL 来获得错误请求的人,而不是让他们知道他们“在正确的轨道上”
在 .NET Core 中,获取证书的方式不再是通过 Module。可能适合您的现代方式是:
private static X509Certificate2? Signer()
using var cert = X509Certificate2.CreateFromSignedFile(Assembly.GetExecutingAssembly().Location);
if (cert is null)
return null;
return new X509Certificate2(cert);
【讨论】:
您如何确认客户端请求是使用 HTTP/1.1 发出的? HTTP/2 中没有客户端证书认证。 我知道这是旧的,但要回答您的评论:handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
【参考方案3】:
我的客户端没有使用 .NET,但服务器端可以通过 IIS 简单地配置它,方法是在 IIS 后面部署我的 ASP.NET Core 网站,为 HTTPS + 客户端证书配置 IIS:
IIS 客户端证书设置:
那么你可以在代码中简单的获取:
var clientCertificate = await HttpContext.Connection.GetClientCertificateAsync();
if(clientCertificate!=null)
return new ContentResult() Content = clientCertificate.Subject ;
它对我来说很好,但我使用 curl 或 chrome 作为客户端,而不是 .NET 客户端。在 HTTPS 握手期间,客户端从服务器获取请求以提供证书并将其发送到服务器。
如果您使用的是 .NET Core 客户端,它不能具有特定于平台的代码,并且如果它无法将自身连接到任何特定于操作系统的证书存储、提取它并将其发送到服务器,这将是有意义的.如果您是针对 .NET 4.5.x 进行编译,那么这似乎很容易:
Using HttpClient with SSL/TLS-based client side authentication
就像你编译 curl 的时候一样。如果您希望能够将其连接到 Windows 证书存储,则必须针对某些特定的 Windows 库进行编译。
【讨论】:
感谢您花时间回答,但我将它用于 asp.net 核心。上面的代码对我有用。【参考方案4】:可用于 .NET Core 2.0
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler?view=netframework-4.7.1
【讨论】:
handler.ClientCertificates.Add(new X509Certificate2("cert.crt")) 仅适用于 Framework 4.8 (docs.microsoft.com/ru-ru/dotnet/api/…) @Kate Miss 我说它不适用于.Net Framework 4.5
.Net Framework 4.7.1。在本地进行了测试,文档也这么说。更新了答案。【参考方案5】:
在对这个问题进行了大量测试后,我最终解决了这个问题。
-
使用
SSL
,我从证书和密钥创建了一个pfx
文件。
如下创建HttpClient
:
_httpClient = new(new HttpClientHandler
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12,
ClientCertificates = new X509Certificate2(@"C:\kambiDev.pfx")
);
【讨论】:
【参考方案6】:像这样在 Main() 中进行所有配置:
public static void Main(string[] args)
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
string env="", sbj="", crtf = "";
try
var whb = WebHost.CreateDefaultBuilder(args).UseContentRoot(Directory.GetCurrentDirectory());
var environment = env = whb.GetSetting("environment");
var subjectName = sbj = CertificateHelper.GetCertificateSubjectNameBasedOnEnvironment(environment);
var certificate = CertificateHelper.GetServiceCertificate(subjectName);
crtf = certificate != null ? certificate.Subject : "It will after the certification";
if (certificate == null) // present apies even without server certificate but dont give permission on authorization
var host = whb
.ConfigureKestrel(_ => )
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseConfiguration(configuration)
.UseSerilog((context, config) =>
config.ReadFrom.Configuration(context.Configuration);
)
.Build();
host.Run();
else
var host = whb
.ConfigureKestrel(options =>
options.Listen(new IPEndPoint(IPAddress.Loopback, 443), listenOptions =>
var httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
ClientCertificateMode = ClientCertificateMode.AllowCertificate,
SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
ServerCertificate = certificate
;
listenOptions.UseHttps(httpsConnectionAdapterOptions);
);
)
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls("https://*:443")
.UseStartup<Startup>()
.UseConfiguration(configuration)
.UseSerilog((context, config) =>
config.ReadFrom.Configuration(context.Configuration);
)
.Build();
host.Run();
Log.Logger.Information("Information: Environment = " + env +
" Subject = " + sbj +
" Certificate Subject = " + crtf);
catch(Exception ex)
Log.Logger.Error("Main handled an exception: Environment = " + env +
" Subject = " + sbj +
" Certificate Subject = " + crtf +
" Exception Detail = " + ex.Message);
像这样配置文件startup.cs:
#region 2way SSL settings
services.AddMvc();
services.AddAuthentication(options =>
options.DefaultAuthenticateScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
)
.AddCertificateAuthentication(certOptions =>
var certificateAndRoles = new List<CertficateAuthenticationOptions.CertificateAndRoles>();
Configuration.GetSection("AuthorizedCertficatesAndRoles:CertificateAndRoles").Bind(certificateAndRoles);
certOptions.CertificatesAndRoles = certificateAndRoles.ToArray();
);
services.AddAuthorization(options =>
options.AddPolicy("CanAccessAdminMethods", policy => policy.RequireRole("Admin"));
options.AddPolicy("CanAccessUserMethods", policy => policy.RequireRole("User"));
);
#endregion
证书助手
public class CertificateHelper
protected internal static X509Certificate2 GetServiceCertificate(string subjectName)
using (var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(
X509FindType.FindBySubjectDistinguishedName, subjectName, true);
X509Certificate2 certificate = null;
if (certCollection.Count > 0)
certificate = certCollection[0];
return certificate;
protected internal static string GetCertificateSubjectNameBasedOnEnvironment(string environment)
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.environment.json", optional: false);
var configuration = builder.Build();
return configuration["ServerCertificateSubject"];
【讨论】:
【参考方案7】:如果您查看.NET Standard reference for the HttpClientHandler class,您可以看到 ClientCertificates 属性存在,但由于使用了EditorBrowsableState.Never 而被隐藏。这会阻止 IntelliSense 显示它,但仍可在使用它的代码中工作。
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates get;
【讨论】:
【参考方案8】:我认为最好的答案是here。
通过使用 X-ARR-ClientCert 标头,您可以提供证书信息。
这里有一个经过调整的解决方案:
X509Certificate2 certificate;
var handler = new HttpClientHandler
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12
;
handler.ClientCertificates.Add(certificate);
handler.CheckCertificateRevocationList = false;
// this is required to get around self-signed certs
handler.ServerCertificateCustomValidationCallback =
(httpRequestMessage, cert, cetChain, policyErrors) =>
return true;
;
var client = new HttpClient(handler);
requestMessage.Headers.Add("X-ARR-ClientCert", certificate.GetRawCertDataString());
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.SendAsync(requestMessage);
if (response.IsSuccessStatusCode)
var responseContent = await response.Content.ReadAsStringAsync();
var keyResponse = JsonConvert.DeserializeObject<KeyResponse>(responseContent);
return keyResponse;
在您的 .net 核心服务器的启动例程中:
public IServiceProvider ConfigureServices(IServiceCollection services)
services.AddCertificateForwarding(options =>
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) =>
X509Certificate2 clientCertificate = null;
try
if (!string.IsNullOrWhiteSpace(headerValue))
var bytes = ConvertHexToBytes(headerValue);
clientCertificate = new X509Certificate2(bytes);
catch (Exception)
// invalid certificate
return clientCertificate;
;
);
【讨论】:
X-ARR-ClientCert 是 Azure 特定的东西,此代码实际上并未验证客户端是否拥有证书的私钥。它只是将公钥附加到请求中。请注意,链接的文章已更正为实际正确使用客户端证书。以上是关于将客户端证书添加到 .NET Core HttpClient的主要内容,如果未能解决你的问题,请参考以下文章
将处理程序添加到 ASP.NET Core 中的默认 http 客户端 [重复]