Microsoft.AspNetCore.Authentication.Certificate
包含类似于 ASP.NET Core证书身份验证的实现。 证书身份验证发生在 TLS 级别,在它被 ASP.NET Core 之前。 更准确地说,这是验证证书的身份验证处理程序,然后向你提供可将该证书解析到 ClaimsPrincipal
的事件。
将主机配置为使用证书进行身份验证,如 IIS、Kestrel、Azure Web 应用,或者其他任何所用的。
证书身份验证是一种有状态方案,主要用于代理或负载均衡器不处理客户端和服务器之间的流量。 如果使用代理或负载平衡器,则仅当代理或负载均衡器:
使用代理和负载平衡器的环境中证书身份验证的替代方法是使用 OpenID Connect (OIDC) Active Directory 联合服务(ADFS)。
获取并应用 HTTPS 证书,并将主机配置为需要证书。
在 web 应用中,添加对 Microsoft.AspNetCore.Authentication.Certificate
包的引用。 然后,在 Startup.ConfigureServices
方法中,使用你的选项调用 services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);
,并为 OnCertificateValidated
提供一个委托,以便在与请求一起发送的客户端证书上执行任何补充验证。 将该信息转换为 ClaimsPrincipal
,并在 context.Principal
属性上对其进行设置。
如果身份验证失败,则此处理程序将返回 403 (Forbidden)
响应,而不是 401 (Unauthorized)
,如你所料。 原因是,在初次 TLS 连接期间应进行身份验证。 当它到达处理程序时,它的时间太晚。 无法将连接从匿名连接升级到证书。
此外,还可以在 Startup.Configure
方法中添加 app.UseAuthentication();
。 否则,不会将 HttpContext.User
设置为从证书创建 ClaimsPrincipal
。 例如:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication( CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(); // All the other service configuration. } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); // All the other app configuration. }
前面的示例演示了添加证书身份验证的默认方法。 处理程序使用通用证书属性构造用户主体。
CertificateAuthenticationOptions
处理程序具有一些内置的验证,这些验证是你应在证书上执行的最小验证。 默认情况下,将启用这些设置中的每一个。
默认值:30CertificateTypes.Chained
此检查将验证是否只允许使用适当的证书类型。 如果应用使用自签名证书,则需要将此选项设置为 CertificateTypes.All
或 CertificateTypes.SelfSigned
。
默认值:30true
此检查将验证客户端提供的证书是否具有客户端身份验证扩展密钥使用(EKU),或者根本没有 Eku。 如规范所示,如果未指定 EKU,则所有 Eku 均视为有效。
默认值:30true
此检查将验证证书是否在其有效期内。 对于每个请求,处理程序将确保在其在其当前会话期间提供的证书过期时,该证书是有效的。
默认值:30X509RevocationFlag.ExcludeRoot
一个标志,该标志指定将检查链中的哪些证书进行吊销。
仅当证书链接到根证书时才执行吊销检查。
默认值:30X509RevocationMode.Online
指定如何执行吊销检查的标志。
指定联机检查可能会导致长时间延迟,同时与证书颁发机构联系。
仅当证书链接到根证书时才执行吊销检查。
这是不可能的。 请记住,证书交换已完成 HTTPS 会话的启动,在该连接上收到第一个请求之前,服务器会完成此操作,因此无法基于任何请求字段进行作用域。
处理程序有两个事件:
OnAuthenticationFailed
–,并允许您做出反应。OnCertificateValidated
–,已创建验证,已创建默认主体。 此事件允许你执行自己的验证并增加或替换主体。 例如:
Startup.ConfigureServices
中的示例:services.AddAuthentication( CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(options => { options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var claims = new[] { new Claim( ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal( new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); return Task.CompletedTask; } }; });
如果发现入站证书不能满足额外的验证,请调用 context.Fail("failure reason")
失败原因。
对于实际功能,你可能需要调用在依赖关系注入中注册的服务,该服务连接到数据库或其他类型的用户存储。 使用传递到委托中的上下文访问你的服务。 请看下面 Startup.ConfigureServices
中的示例:
services.AddAuthentication( CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(options => { options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var validationService = context.HttpContext.RequestServices .GetService<ICertificateValidationService>(); if (validationService.ValidateCertificate( context.ClientCertificate)) { var claims = new[] { new Claim( ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim( ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal( new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); } return Task.CompletedTask; } }; });
从概念上讲,验证证书是一种授权问题。 例如,在授权策略中添加一个颁发者或指纹(而不是 OnCertificateValidated
)是完全可以接受的。
在Program.cs中,按如下所示配置 Kestrel:
public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.ConfigureKestrel(o => { o.ConfigureHttpsDefaults(o => o.ClientCertificateMode = ClientCertificateMode.RequireCertificate); }); }); }
备注
通过在调用 Listen 之前调用 创建的终结点将不会应用默认值。 ConfigureHttpsDefaults
在 IIS 管理器中完成以下步骤:
有关如何配置证书转发中间件的详细说明,请参阅托管和部署文档。
Azure 不需要转发配置。 此设置已在证书转发中间件中进行设置。
备注
这要求存在 CertificateForwardingMiddleware。
AddCertificateForwarding
方法用于指定:
HeaderConverter
属性)。在自定义 web 代理中,证书作为自定义请求标头(例如 X-SSL-CERT
)传递。 若要使用它,请在 Startup.ConfigureServices
中配置证书转发:
public void ConfigureServices(IServiceCollection services) { services.AddCertificateForwarding(options => { options.CertificateHeader = "X-SSL-CERT"; options.HeaderConverter = (headerValue) => { X509Certificate2 clientCertificate = null; if(!string.IsNullOrWhiteSpace(headerValue)) { byte[] bytes = StringToByteArray(headerValue); clientCertificate = new X509Certificate2(bytes); } return clientCertificate; }; }); } private static byte[] StringToByteArray(string hex) { int NumberChars = hex.Length; byte[] bytes = new byte[NumberChars / 2]; for (int i = 0; i < NumberChars; i += 2) { bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); } return bytes; }
然后 Startup.Configure
方法添加中间件。 在调用 UseAuthentication
和 UseAuthorization
之前调用 UseCertificateForwarding
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... app.UseRouting(); app.UseCertificateForwarding(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
单独的类可用于实现验证逻辑。 由于本示例中使用了相同的自签名证书,因此请确保只能使用证书。 验证客户端证书和服务器证书的指纹是否匹配,否则,任何证书都可以使用,并且足以进行身份验证。 这会在 AddCertificate
方法中使用。 如果使用的是中间证书或子证书,也可以在此处验证使用者或颁发者。
using System.IO; using System.Security.Cryptography.X509Certificates; namespace AspNetCoreCertificateAuthApi { public class MyCertificateValidationService { public bool ValidateCertificate(X509Certificate2 clientCertificate) { // Do not hardcode passwords in production code // Use thumbprint or key vault var cert = new X509Certificate2( Path.Combine("sts_dev_cert.pfx"), "1234"); if (clientCertificate.Thumbprint == cert.Thumbprint) { return true; } return false; } } }
HttpClientHandler 可以直接添加到 HttpClient 类的构造函数中。 创建 HttpClient 的实例时应格外小心。 然后,HttpClient 将随每个请求发送证书。
private async Task<JsonDocument> GetApiDataUsingHttpClientHandler() { var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234"); var handler = new HttpClientHandler(); handler.ClientCertificates.Add(cert); var client = new HttpClient(handler); var request = new HttpRequestMessage() { RequestUri = new Uri("https://localhost:44379/api/values"), Method = HttpMethod.Get, }; var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(responseContent); return data; } throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}"); }
在下面的示例中,使用处理程序中的 ClientCertificates 属性将客户端证书添加到 HttpClientHandler 中。 然后,可以使用 ConfigurePrimaryHttpMessageHandler 方法在 HttpClient 的命名实例中使用此处理程序。 这是在 ConfigureServices 方法中的 Startup 类中设置的。
var clientCertificate = new X509Certificate2( Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234"); var handler = new HttpClientHandler(); handler.ClientCertificates.Add(clientCertificate); services.AddHttpClient("namedClient", c => { }).ConfigurePrimaryHttpMessageHandler(() => handler);
然后,可以使用 IHttpClientFactory 通过处理程序和证书获取命名实例。 使用 Startup 类中定义的客户端名称的 CreateClient 方法来获取实例。 可根据需要使用客户端发送 HTTP 请求。
private readonly IHttpClientFactory _clientFactory; public ApiService(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } private async Task<JsonDocument> GetApiDataWithNamedClient() { var client = _clientFactory.CreateClient("namedClient"); var request = new HttpRequestMessage() { RequestUri = new Uri("https://localhost:44379/api/values"), Method = HttpMethod.Get, }; var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); var data = JsonDocument.Parse(responseContent); return data; } throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}"); }
如果将正确的证书发送到服务器,则返回数据。 如果未发送证书或证书不正确,则返回 HTTP 403 状态代码。
创建证书是最难设置此流的部分。 可以使用 New-SelfSignedCertificate
PowerShell cmdlet 创建根证书。 创建证书时,请使用强密码。 添加 KeyUsageProperty
参数和 KeyUsage
参数非常重要,如下所示。
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature $mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt
备注
-DnsName
参数值必须与应用的部署目标匹配。 例如,用于开发的 "localhost"。
根证书需要在主机系统上受信任。 默认情况下,不信任证书颁发机构创建的根证书。 下面的链接说明了如何在 Windows 上完成此操作:
现在可以从根证书创建中间证书。 这并不是所有用例所必需的,但你可能需要创建多个证书,或者需要激活或禁用证书组。 需要 TextExtension
参数以设置证书的基本约束中的路径长度。
然后,可以将中间证书添加到 Windows 主机系统中的受信任中间证书。
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText $parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." ) New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1") Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt
可以从中间证书创建子证书。 这是最终实体,不需要创建更多的子证书。
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." ) New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" $mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
还可以直接从根证书创建子证书。
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." ) New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" $mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText $mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt $rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 ) New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1") Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt $parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 ) New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt
使用根证书、中间证书或子证书时,可以根据需要使用指纹或 PublicKey 来验证证书。
using System.Collections.Generic; using System.IO; using System.Security.Cryptography.X509Certificates; namespace AspNetCoreCertificateAuthApi { public class MyCertificateValidationService { public bool ValidateCertificate(X509Certificate2 clientCertificate) { return CheckIfThumbprintIsValid(clientCertificate); } private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate) { var listOfValidThumbprints = new List<string> { "141594A0AE38CBBECED7AF680F7945CD51D8F28A", "0C89639E4E2998A93E423F919B36D4009A0F9991", "BA9BF91ED35538A01375EFC212A2F46104B33A44" }; if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint)) { return true; } return false; } } }