You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();

// ...路由配置

关键说明

  1. 禁用默认校验后,自定义的AuthenticationService需要完全负责证书的所有校验逻辑(包括有效期、信任链等),你的现有代码已经包含了有效期校验,这部分没问题。
  2. UseAuthorization必须在UseAuthentication之后添加,否则认证结果不会被授权逻辑校验,导致即使认证失败也能访问接口。
  3. 捕获IsValidClientCertificate抛出的异常,调用context.Fail(),避免未处理异常导致请求中断而不是返回认证失败响应。

内容的提问来源于stack exchange,提问作者Matthew Hamilton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:13:11