c# - 如何使用客户端证书在 Web API 中进行身份验证和授权

标签 c# security asp.net-web-api ssl-certificate client-certificates

我正在尝试使用客户端证书通过 Web API 对设备进行身份验证和授权,并开发了一个简单的概念证明来解决潜在解决方案的问题。我遇到了 Web 应用程序未收到客户端证书的问题。许多人报告了这个问题,including in this Q&A ,但他们都没有答案。我希望提供更多细节来重振这个问题,并希望得到我的问题的答案。我对其他解决方案持开放态度。主要要求是用 C# 编写的独立进程可以调用 Web API 并使用客户端证书进行身份验证。

这个 POC 中的 Web API 非常简单,只返回一个值。它使用一个属性来验证是否使用了 HTTPS 以及是否存在客户端证书。

public class SecureController : ApiController
{
    [RequireHttps]
    public string Get(int id)
    {
        return "value";
    }

}

这是 RequireHttpsAttribute 的代码:
public class RequireHttpsAttribute : AuthorizationFilterAttribute 
{ 
    public override void OnAuthorization(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) 
        { 
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) 
            { 
                ReasonPhrase = "HTTPS Required" 
            }; 
        } 
        else 
        {
            var cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                }; 

            }
            base.OnAuthorization(actionContext); 
        } 
    } 
}

在这个 POC 中,我只是检查客户端证书的可用性。一旦这个工作正常,我可以在证书中添加信息检查以根据证书列表进行验证。

以下是此 Web 应用程序在 IIS 中的 SSL 设置。

enter image description here

这是发送带有客户端证书的请求的客户端的代码。这是一个控制台应用程序。
    private static async Task SendRequestUsingHttpClient()
    {
        WebRequestHandler handler = new WebRequestHandler();
        X509Certificate certificate = GetCert("ClientCertificate.cer");
        handler.ClientCertificates.Add(certificate);
        handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://localhost:44398/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.GetAsync("api/Secure/1");
            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("Received response: {0}",content);
            }
            else
            {
                Console.WriteLine("Error, received status code {0}: {1}", response.StatusCode, response.ReasonPhrase);
            }
        }
    }

    public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
    {
        Console.WriteLine("Validating certificate {0}", certificate.Issuer);
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;

        Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

当我运行这个测试应用程序时,我得到一个 403 Forbidden 状态代码,原因短语是“需要客户端证书”,表明它正在进入我的 RequireHttpsAttribute 并且没有找到任何客户端证书。通过调试器运行此程序,我已验证证书已加载并添加到 WebRequestHandler。证书将导出到正在加载的 CER 文件中。带有私钥的完整证书位于 Web 应用程序服务器的本地计算机的个人和可信根存储中。对于此测试,客户端和 Web 应用程序在同一台机器上运行。

我可以使用 Fiddler 调用这个 Web API 方法,附加相同的客户端证书,它工作正常。使用 Fiddler 时,它通过了 RequireHttpsAttribute 中的测试并返回成功状态代码 200 并返回预期值。

有没有人遇到过同样的问题,即 HttpClient 没有在请求中发送客户端证书并找到解决方案?

更新 1:

我还尝试从包含私钥的证书存储中获取证书。这是我检索它的方法:
    private static X509Certificate2 GetCert2(string hostname)
    {
        X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        myX509Store.Open(OpenFlags.ReadWrite);
        X509Certificate2 myCertificate = myX509Store.Certificates.OfType<X509Certificate2>().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);
        return myCertificate;
    }

我确认此证书已正确检索,并且已添加到客户端证书集合中。但是我得到了相同的结果,其中服务器代码不检索任何客户端证书。

为了完整起见,这里是用于从文件中检索证书的代码:
    private static X509Certificate GetCert(string filename)
    {
        X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);
        return Cert;

    }

您会注意到,当您从文件中获取证书时,它返回一个类型为 X509Certificate 的对象,而当您从证书存储中检索它时,它的类型为 X509Certificate2。 X509CertificateCollection.Add 方法需要 X509Certificate 类型。

更新 2:
我仍在试图弄清楚这一点,并尝试了许多不同的选择,但无济于事。
  • 我将 Web 应用程序更改为在主机名而不是本地主机上运行。
  • 我将 Web 应用程序设置为需要 SSL
  • 我确认证书是为客户端身份验证设置的,并且它位于受信任的根
  • 中。
  • 除了在 Fiddler 中测试客户端证书之外,我还在 Chrome 中对其进行了验证。

  • 在尝试这些选项的某个时刻,它开始工作。然后我开始撤回更改以查看是什么导致它起作用。它继续工作。然后我尝试从受信任的根中删除证书以验证这是必需的,并且它停止工作,现在即使我将证书放回受信任的根中,我也无法让它恢复工作。现在 Chrome 甚至不会提示我输入类似它使用的证书,它在 Chrome 中失败,但在 Fiddler 中仍然有效。我一定缺少一些神奇的配置。

    我还尝试在绑定(bind)中启用“协商客户端证书”,但 Chrome 仍然不会提示我输入客户端证书。这是使用“netsh http show sslcert”的设置
     IP:port                 : 0.0.0.0:44398
     Certificate Hash        : 429e090db21e14344aa5d75d25074712f120f65f
     Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
     Certificate Store Name  : MY
     Verify Client Certificate Revocation    : Disabled
     Verify Revocation Using Cached Client Certificate Only    : Disabled
     Usage Check    : Enabled
     Revocation Freshness Time : 0
     URL Retrieval Timeout   : 0
     Ctl Identifier          : (null)
     Ctl Store Name          : (null)
     DS Mapper Usage    : Disabled
     Negotiate Client Certificate    : Enabled
    

    这是我正在使用的客户端证书:

    enter image description here

    enter image description here

    enter image description here

    我对问题是什么感到困惑。我正在为任何可以帮助我解决这个问题的人增加赏金。

    最佳答案

    跟踪帮助我找到了问题所在(感谢 Fabian 的建议)。我发现通过进一步的测试,我可以让客户端证书在另一台服务器(Windows Server 2012)上工作。我在我的开发机器(Window 7)上测试这个,所以我可以调试这个过程。因此,通过将跟踪与运行的 IIS 服务器和未运行的 IIS 服务器进行比较,我能够查明跟踪日志中的相关行。这是客户端证书工作的日志的一部分。这是发送前的设置

    System.Net Information: 0 : [17444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
    System.Net Information: 0 : [17444] SecureChannel#54718731 - We have user-provided certificates. The server has not specified any issuers, so try all the certificates.
    System.Net Information: 0 : [17444] SecureChannel#54718731 - Selected certificate:
    

    这是客户端证书失败的机器上的跟踪日志的样子。
    System.Net Information: 0 : [19616] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
    System.Net Information: 0 : [19616] SecureChannel#54718731 - We have user-provided certificates. The server has specified 137 issuer(s). Looking for certificates that match any of the issuers.
    System.Net Information: 0 : [19616] SecureChannel#54718731 - Left with 0 client certificates to choose from.
    System.Net Information: 0 : [19616] Using the cached credential handle.
    

    关注指示服务器指定 137 个发行者的行,我发现了这个 Q&A that seemed similar to my issue .我的解决方案不是标记为答案的解决方案,因为我的证书位于受信任的根中。答案是the one under it您更新注册表的位置。我刚刚将该值添加到注册表项中。

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL

    值名称:SendTrustedIssuerList 值类型:REG_DWORD 值数据:0(假)

    将此值添加到注册表后,它开始在我的 Windows 7 机器上工作。这似乎是 Windows 7 问题。

    关于c# - 如何使用客户端证书在 Web API 中进行身份验证和授权,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35582396/

    相关文章:

    c# - .NET Core 项目与 .NET Framework 中为 System.Data.DataTable 生成的 JSON 的变化

    c# - 如何为 Windows Phone 创建应用程序文件(例如 .apk)?

    java - 如何在 Android 应用程序中保护 secret 字符串?

    c# - 保护 Winform 页面的最佳方式?

    javascript - 尝试显示 Web api 服务数据时出现空白页面

    asp.net-web-api - 在 Meteor 中调用外部 Web API 有多容易?

    c# - 在 Controller 体内访问 Web API 反序列化/序列化方法

    c# - 获取文件的相对路径 C#

    c# - Automapper:从单个源值填充多个目标值

    c# - 清理发往电子邮件的用户输入