在 Windows 应用之间共享证书

需要超出用户 ID 和密码组合的安全身份验证的 Windows 应用可以使用证书进行身份验证。 对用户进行身份验证时,证书身份验证将提供高级别的信任。 在某些情况下,一组服务将要针对多个应用对用户进行身份验证。 本文介绍如何使用同一证书对多个 Windows 应用进行身份验证,以及如何为用户提供一种方法来导入为访问安全 Web 服务而提供的证书。

应用可使用证书对 Web 服务进行身份验证,并且多个应用可使用来自证书存储的单个证书对相同的用户进行身份验证。 如果存储中不存在证书,可将代码添加到应用以从 PFX 文件导入证书。 本快速入门中的客户端应用是 WinUI 应用,Web 服务是 ASP.NET 核心 Web API。

提示

如果你对开始编写 Windows 应用或 ASP.NET Core Web API 有疑问,Microsoft Copilot 是一个很好的资源。 Copilot 可以帮助你编写代码、查找示例,并了解有关创建安全应用的最佳做法的详细信息。

先决条件

创建并发布安全的 Web 服务

  1. 打开Microsoft Visual Studio,然后从“开始”屏幕中选择“ 创建新项目 ”。

  2. 在“创建新项目”对话框中,在“选择项目类型”下拉列表中选择 API 以筛选可用的项目模板。

  3. 选择“ASP.NET Core Web API”模板,然后选择“下一步”。

  4. 将应用程序命名为“FirstContosoBank”,然后选择“ 下一步”。

  5. 选择 .NET 8.0 或更高版本作为 框架,将 身份验证类型 设置为 “无”,确保 选中“配置 HTTPS ”,取消选中“ 启用 OpenAPI 支持”,选中 “不使用顶级语句 和使用 控制器”,然后选择“ 创建”。

    Visual Studio 为 ASP.NET Core Web API 项目创建新项目详细信息的屏幕截图

  6. 右键单击“控制器”文件夹中的WeatherForecastController.cs文件,然后选择“重命名”。 将名称更改为 BankController.cs ,让 Visual Studio 重命名类和对该类的所有引用。

  7. launchSettings.json 文件中,将“launchUrl”的值从“weatherforecast”更改为“bank”,以用于所有三种配置的值。

  8. BankController.cs 文件中,添加以下“Login”方法。

    [HttpGet]
    [Route("login")]
    public string Login()
    {
        // Return any value you like here.
        // The client is just looking for a 200 OK response.
        return "true";
    }
    
  9. 打开 NuGet 程序包管理器并搜索并安装 Microsoft.AspNetCore.Authentication.Certificate 包的最新稳定版本。 此包为 ASP.NET Core 中的证书身份验证提供中间件。

  10. 向名为 SecureCertificateValidationService 的项目添加新类。 将以下代码添加到类以配置证书身份验证中间件。

    using System.Security.Cryptography.X509Certificates;
    
    public class SecureCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            // Values are hard-coded for this example.
            // You should load your valid thumbprints from a secure location.
            string[] allowedThumbprints = { "YOUR_CERTIFICATE_THUMBPRINT_1", "YOUR_CERTIFICATE_THUMBPRINT_2" };
            if (allowedThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }
        }
    }
    
  11. 打开Program.cs,并将 Main 方法中的代码替换为以下代码:

    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
    
        // Add our certificate validation service to the DI container.
        builder.Services.AddTransient<SecureCertificateValidationService>();
    
        builder.Services.Configure<KestrelServerOptions>(options =>
        {
            // Configure Kestrel to require a client certificate.
            options.ConfigureHttpsDefaults(options =>
            {
                options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
                options.AllowAnyClientCertificate();
            });
        });
    
        builder.Services.AddControllers();
    
        // Add certificate authentication middleware.
        builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
           .AddCertificate(options =>
        {
            options.AllowedCertificateTypes = CertificateTypes.SelfSigned;
            options.Events = new CertificateAuthenticationEvents
            {
                // Validate the certificate with the validation service.
                OnCertificateValidated = context =>
                {
                    var validationService = context.HttpContext.RequestServices.GetService<SecureCertificateValidationService>();
    
                    if (validationService.ValidateCertificate(context.ClientCertificate))
                    {
                        context.Success();
                    }
                    else
                    {
                        context.Fail("Invalid certificate");
                    }
    
                    return Task.CompletedTask;
                },
                OnAuthenticationFailed = context =>
                {
                    context.Fail("Invalid certificate");
                    return Task.CompletedTask;
                }
            };
         });
    
         var app = builder.Build();
    
         // Add authentication/authorization middleware.
         app.UseHttpsRedirection();
         app.UseAuthentication();
         app.UseAuthorization();
    
         app.MapControllers();
         app.Run();
     }
    

    上面的代码将 Kestrel 服务器配置为要求客户端证书,并将证书身份验证中间件添加到应用。 中间件使用 SecureCertificateValidationService 类验证客户端证书。 OnCertificateValidated验证证书时会调用该事件。 如果证书有效,事件将调用该方法 Success 。 如果证书无效,事件将使用错误消息调用 Fail 该方法,该错误消息将返回到客户端。

  12. 开始调试项目以启动 Web 服务。 你可能会收到有关信任和安装 SSL 证书的消息。 对于每个消息,单击“是可信任证书并继续调试项目。

    询问用户是否要信任证书的对话框的屏幕截图

    Windows 对话框的屏幕截图,询问用户是否要安装证书

  13. Web 服务将在 https://localhost:7072/bank. 可以通过打开 Web 浏览器并输入 Web 地址来测试 Web 服务。 你将看到生成的天气预报数据的格式为 JSON。 创建客户端应用时,使 Web 服务保持运行。

有关使用基于 ASP.NET 核心控制器的 Web API 的详细信息,请参阅 使用 ASP.NET Core 创建 Web API。

创建使用证书身份验证的 WinUI 应用

现在你已拥有一个或多个安全 Web 服务,应用可使用证书来验证这些 Web 服务。 使用 WinRT API 中的 HttpClient 对象向经过身份验证的 Web 服务发出请求时,初始请求将不包含客户端证书。 经过身份验证的 Web 服务将以对客户端身份验证的请求进行响应。 当此情况发生时,Windows 客户端将自动查询证书存储以获取可用的客户端证书。 用户可以从这些证书中选择以对 Web 服务进行身份验证。 一些证书受密码保护,所以你将需要向用户提供输入证书密码的方法。

注意

目前还没有用于管理证书的Windows 应用 SDK API。 必须使用 WinRT API 来管理应用中的证书。 我们还将使用 WinRT 存储 API 从 PFX 文件导入证书。 任何具有包标识的 Windows 应用(包括 WinUI 应用)都可以使用许多 WinRT API。

我们将实现的 HTTP 客户端代码使用 。NET 的 HttpClientWinRT API 中包含的 HttpClient 不支持客户端证书。

如果没有可用的客户端证书,则用户将需要将证书添加到证书存储。 可将代码包括在 Windows 应用商店应用中,此应用使用户能够选择包含客户端证书的 PFX 文件,然后将该证书导入到客户端证书存储中。

提示

可以使用 PowerShell cmdlet New-SelfSignedCertificate 和 Export-PfxCertificate 创建自签名证书,并将其导出到 PFX 文件以用于本快速入门。 有关信息,请参阅 New-SelfSignedCertificateExport-PfxCertificate

请注意,生成证书时,应保存证书的指纹,以便在 Web 服务中用于验证。

  1. 打开 Visual Studio 并从起始页创建新的 WinUI 项目。 将此新项目命名为“FirstContosoBankApp”。 单击“创建”以创建新项目

  2. MainWindow.xaml 文件中,将以下 XAML 添加到 Grid 元素,替换现有的 StackPanel 元素及其内容。 此 XAML 包括一个用于浏览要导入的 PFX 文件的按钮、一个用于输入受密码保护的 PFX 文件的密码的文本框、一个用于导入选中的 PFX 文件的按钮、一个用于登录安全 Web 服务的按钮以及一个用于显示当前操作状况的文本块。

    <Button x:Name="Import" Content="Import Certificate (PFX file)" HorizontalAlignment="Left" Margin="352,305,0,0" VerticalAlignment="Top" Height="77" Width="260" Click="Import_Click" FontSize="16"/>
    <Button x:Name="Login" Content="Login" HorizontalAlignment="Left" Margin="611,305,0,0" VerticalAlignment="Top" Height="75" Width="240" Click="Login_Click" FontSize="16"/>
    <TextBlock x:Name="Result" HorizontalAlignment="Left" Margin="355,398,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Height="153" Width="560"/>
    <PasswordBox x:Name="PfxPassword" HorizontalAlignment="Left" Margin="483,271,0,0" VerticalAlignment="Top" Width="229"/>
    <TextBlock HorizontalAlignment="Left" Margin="355,271,0,0" TextWrapping="Wrap" Text="PFX password" VerticalAlignment="Top" FontSize="18" Height="32" Width="123"/>
    <Button x:Name="Browse" Content="Browse for PFX file" HorizontalAlignment="Left" Margin="352,189,0,0" VerticalAlignment="Top" Click="Browse_Click" Width="499" Height="68" FontSize="16"/>
    <TextBlock HorizontalAlignment="Left" Margin="717,271,0,0" TextWrapping="Wrap" Text="(Optional)" VerticalAlignment="Top" Height="32" Width="83" FontSize="16"/>
    
  3. 保存 MainWindow 更改。

  4. 打开 MainWindow.xaml.cs 文件,并添加以下 using 语句。

    using System;
    using System.Security.Cryptography.X509Certificates;
    using System.Diagnostics;
    using System.Net.Http;
    using System.Net;
    using System.Text;
    using Microsoft.UI.Xaml;
    using Windows.Security.Cryptography.Certificates;
    using Windows.Storage.Pickers;
    using Windows.Storage;
    using Windows.Storage.Streams;
    
  5. 在MainWindow.xaml.cs文件中,将以下变量添加到 MainWindow 类。 它们指定“FirstContosoBank”Web 服务的安全 登录 服务终结点的地址,以及保存要导入到证书存储中的 PFX 证书的全局变量。 更新 <server-name> localhost:7072 API 项目的launchSettings.json文件中的“https”配置中指定的端口或端口。

    private Uri requestUri = new Uri("https://<server-name>/bank/login");
    private string pfxCert = null;
    
  6. MainWindow.xaml.cs 文件中,为登录按钮和访问安全 Web 服务的方法添加以下单击处理程序。

    private void Login_Click(object sender, RoutedEventArgs e)
    {
        MakeHttpsCall();
    }
    
    private async void MakeHttpsCall()
    {
        var result = new StringBuilder("Login ");
    
        // Load the certificate
        var certificate = new X509Certificate2(Convert.FromBase64String(pfxCert),
                                               PfxPassword.Password);
    
        // Create HttpClientHandler and add the certificate
        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(certificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Automatic;
    
        // Create HttpClient with the handler
        var client = new HttpClient(handler);
    
        try
        {
            // Make a request
            var response = await client.GetAsync(requestUri);
    
            if (response.StatusCode == HttpStatusCode.OK)
            {
                result.Append("successful");
            }
            else
            {
                result = result.Append("failed with ");
                result = result.Append(response.StatusCode);
            }
        }
        catch (Exception ex)
        {
            result = result.Append("failed with ");
            result = result.Append(ex.Message);
        }
    
        Result.Text = result.ToString();
    }
    
  7. 接下来,为该按钮添加以下单击处理程序以浏览 PFX 文件和将所选 PFX 文件导入证书存储中的按钮。

    private async void Import_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            Result.Text = "Importing selected certificate into user certificate store....";
            await CertificateEnrollmentManager.UserCertificateEnrollmentManager.ImportPfxDataAsync(
                  pfxCert,
                  PfxPassword.Password,
                  ExportOption.Exportable,
                  KeyProtectionLevel.NoConsent,
                  InstallOptions.DeleteExpired,
                  "Import Pfx");
    
            Result.Text = "Certificate import succeeded";
        }
        catch (Exception ex)
        {
            Result.Text = "Certificate import failed with " + ex.Message;
        }
    }
    
    private async void Browse_Click(object sender, RoutedEventArgs e)
    {
        var result = new StringBuilder("Pfx file selection ");
        var pfxFilePicker = new FileOpenPicker();
        IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
        WinRT.Interop.InitializeWithWindow.Initialize(pfxFilePicker, hwnd);
        pfxFilePicker.FileTypeFilter.Add(".pfx");
        pfxFilePicker.CommitButtonText = "Open";
        try
        {
            StorageFile pfxFile = await pfxFilePicker.PickSingleFileAsync();
            if (pfxFile != null)
            {
                IBuffer buffer = await FileIO.ReadBufferAsync(pfxFile);
                using (DataReader dataReader = DataReader.FromBuffer(buffer))
                {
                    byte[] bytes = new byte[buffer.Length];
                    dataReader.ReadBytes(bytes);
                    pfxCert = System.Convert.ToBase64String(bytes);
                    PfxPassword.Password = string.Empty;
                    result.Append("succeeded");
                }
            }
            else
            {
                result.Append("failed");
            }
        }
        catch (Exception ex)
        {
            result.Append("failed with ");
            result.Append(ex.Message); ;
        }
    
        Result.Text = result.ToString();
    }
    
  8. 打开 Package.appxmanifest 文件,并将以下功能添加到“功能”选项卡。

    • EnterpriseAuthentication
    • SharedUserCertificates
  9. 运行应用并登录到安全 Web 服务以及将 PFX 文件导入到本地证书存储中。

    WinUI 应用的屏幕截图,其中包含用于浏览 PFX 文件、导入证书并登录到安全 Web 服务的按钮

可使用这些步骤创建多个应用,这些应用使用同一个用户证书访问相同或不同的安全 Web 服务。

Windows Hello

安全和标识

使用 ASP.NET Core 创建 Web API