ASP.NET Core 8.0 RESTful API证书认证异常:context.Fail不生效
问题描述
我开发了一个类库项目,完全按照微软官方文档实现证书认证,但遇到一个问题:当自定义认证处理器调用context.Fail(message)返回失败状态时,该结果被忽略,证书认证仍会通过。
我的需求是限制仅允许特定证书访问,因此自定义认证处理器会将客户端提供的证书与允许列表(通过主题+颁发者组合校验)比对,处理器内的逻辑没问题,但调用端不识别返回的失败状态。
该类库被ASP.NET Core Web API引用,通过ServiceCollectionExtensions类的方法在Program.cs中配置证书认证,相关代码如下:
ClientConfigurationSettings.cs
namespace Corp.Lib.CertificateAuthentication.Configuration { public class ClientCertificateSettings { public bool CheckThumbprintInCertStore { get; set; } public List<ClientCertificate> AllowedCertificates { get; set; } = null!; } public class ClientCertificate { public string Subject { get; set; } = null!; public string Issuer { get; set; } = null!; } }
ServiceCollectionExtensions.cs
using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using Corp.Lib.CertificateAuthentication.Configuration; using Corp.Lib.CertificateAuthentication.Services; using Corp.Lib.CertificateAuthentication.Services.Interfaces; using Microsoft.AspNetCore.Authentication.Certificate; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.Extensions.DependencyInjection; namespace Corp.Lib.CertificateAuthentication.Extensions { public static class ServiceCollectionExtensions { public static void AddCertificateAuthenticationService(this WebApplicationBuilder builder) { builder.Services.Configure<ClientCertificateSettings>(builder.Configuration.GetSection("ClientCertificateSettings")); // 要求所有请求的客户端必须提供证书 builder.WebHost.ConfigureKestrel(kestrel => { kestrel.ConfigureHttpsDefaults(defaults => { defaults.ClientCertificateMode = ClientCertificateMode.RequireCertificate; //defaults.ClientCertificateValidation = (certificate, chain, errors) => //{ // return true; //}; }); }); builder.Services.AddSingleton<IAuthenticationService, AuthenticationService>(); builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options => { options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var validationService = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationService>(); if (!validationService.IsValidClientCertificate(context.ClientCertificate)) { context.Principal = null; context.Fail("Invalid client certificate."); } else { var claims = new[] { new Claim( ClaimTypes.NameIdentifier, context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false), ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim( ClaimTypes.Name, context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false), ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); } return Task.CompletedTask; } }; }); } public static void UseCertificateAuthenticationService(this WebApplication app) { app.UseAuthentication(); } } }
IAuthenticationService.cs
using System.Security.Cryptography.X509Certificates; namespace Corp.Lib.CertificateAuthentication.Services.Interfaces { public interface IAuthenticationService { bool IsValidClientCertificate(X509Certificate2 certificate); } }
AuthenticationService.cs
using System.Configuration; using Corp.Lib.CertificateAuthentication.Services.Interfaces; using System.Security.Cryptography.X509Certificates; using Corp.Lib.CertificateAuthentication.Configuration; using Microsoft.Extensions.Options; using Corp.Lib.Logging; namespace Corp.Lib.CertificateAuthentication.Services { public class AuthenticationService(IOptions<ClientCertificateSettings> options) : IAuthenticationService { private readonly ClientCertificateSettings _ClientCertificateSettings = options.Value; public bool IsValidClientCertificate(X509Certificate2 clientCertificate) { if (clientCertificate == null!) { var error = new ConfigurationErrorsException("Missing certificate or certificate not sent by client."); Logger.Log.Error(error, "Certificate validation failed. Missing certificate or certificate not sent by client."); throw error; } if (_ClientCertificateSettings == null! || _ClientCertificateSettings.AllowedCertificates == null! || !_ClientCertificateSettings.AllowedCertificates.ToList().Any()) { var error = new ConfigurationErrorsException("Certificate configuration missing. Check AppSettings."); Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing. Check AppSettings."); throw error; } if (_ClientCertificateSettings.AllowedCertificates.Any(cert => string.IsNullOrEmpty(cert.Subject))) { var error = new ConfigurationErrorsException("Certificate configuration missing Subject. Check AppSettings."); Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing Subject. Check AppSettings."); throw error; } if (_ClientCertificateSettings.AllowedCertificates.Any(cert => string.IsNullOrEmpty(cert.Issuer))) { var error = new ConfigurationErrorsException("Certificate configuration missing Subject. Check AppSettings."); Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing Issuer. Check AppSettings."); throw error; } // 1. 检查证书有效期 if (DateTime.Compare(DateTime.UtcNow, clientCertificate.NotBefore) < 0 || DateTime.Compare(DateTime.UtcNow, clientCertificate.NotAfter) > 0) { Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} is expired."); return false; } // 2. 检查证书主题 var foundSubject = false; var certSubjectData = clientCertificate.Subject.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (certSubjectData.Any(certSubject => _ClientCertificateSettings.AllowedCertificates.Any(cert => cert.Subject.Equals(certSubject.Trim(), StringComparison.InvariantCultureIgnoreCase)))) { foundSubject = true; } if (!foundSubject) { Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} does not have a matching Subject."); return false; } // 3. 检查证书颁发者 var certIssuerData = clientCertificate.Issuer.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var foundIssuer = certIssuerData.Any(issuerData => _ClientCertificateSettings.AllowedCertificates.Any(cert => cert.Issuer.Equals(issuerData.Trim(), StringComparison.InvariantCultureIgnoreCase))); if (!foundIssuer) { Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} does not have a matching Issuer."); return false; } // 检查证书是否存在于个人证书存储中 if (_ClientCertificateSettings.CheckThumbprintInCertStore) { var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); try { store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly); var certs = store.Certificates.Find(X509FindType.FindByThumbprint, clientCertificate.Thumbprint, true); if (certs.Count == 0) { Logger.Log.Warning("Invalid client certificate. The thumbprint does not match with a certificate in the certificate store. {@ClientCertificate}", clientCertificate); return false; } } catch (Exception ex) { Logger.Log.Error(ex, "An exception occurred searching for the client certificate in the certificate store. {@ClientCertificate}", clientCertificate); throw; } finally { store.Close(); store.Dispose(); } } return true; } } }
解决方案
问题出在AddCertificate的默认配置上:默认情况下,证书认证中间件会先完成基础的证书有效性校验(比如链信任、有效期等),如果基础校验通过,即使你在OnCertificateValidated里调用context.Fail(),中间件仍可能将认证标记为成功。
要让自定义校验的失败结果生效,需要修改配置,禁用默认的证书校验逻辑,让自定义逻辑完全接管认证判断:
修改ServiceCollectionExtensions.cs中的AddCertificate配置:
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options => { // 禁用默认的证书校验,完全由自定义逻辑处理 options.AllowedCertificateTypes = CertificateTypes.All; options.ValidateCertificateUse = false; options.ValidateValidityPeriod = false; options.RevocationMode = X509RevocationMode.NoCheck; options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var validationService = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationService>(); try { if (!validationService.IsValidClientCertificate(context.ClientCertificate)) { context.Principal = null; context.Fail("Invalid client certificate."); } else { var claims = new[] { new Claim( ClaimTypes.NameIdentifier, context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false), ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim( ClaimTypes.Name, context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false), ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); } } catch (Exception ex) { context.Fail($"Certificate validation failed: {ex.Message}"); } return Task.CompletedTask; } }; });
另外,确保Web API项目中启用了授权中间件,因为仅启用认证(UseAuthentication)不够,还需要添加UseAuthorization来强制校验认证结果:
在Program.cs中:
var app = builder.Build(); // ...其他中间件配置 app.UseCertificateAuthenticationService(); // 添加授权中间件 app.UseAuthorization(); // ...路由配置
关键说明
- 禁用默认校验后,自定义的
AuthenticationService需要完全负责证书的所有校验逻辑(包括有效期、信任链等),你的现有代码已经包含了有效期校验,这部分没问题。 UseAuthorization必须在UseAuthentication之后添加,否则认证结果不会被授权逻辑校验,导致即使认证失败也能访问接口。- 捕获
IsValidClientCertificate抛出的异常,调用context.Fail(),避免未处理异常导致请求中断而不是返回认证失败响应。
内容的提问来源于stack exchange,提问作者Matthew Hamilton
相关产品推荐
相关产品推荐

