使用 ASP.NET Core 安全地存储开发中的应用机密

作者:Rick AndersonKirk Larkin

查看或下载示例代码如何下载

本文档介绍了如何管理开发计算机上 ASP.NET Core 应用的敏感数据。 切勿在源代码中存储密码或其他敏感数据。 不得将生产机密用于开发或测试。 机密不得随应用一起部署。 相反,应通过受控方式(例如环境变量或 Azure Key Vault)访问生产机密。 可使用 Azure Key Vault 配置提供程序存储和保护 Azure 测试和生产机密。

若要在 .NET 控制台应用中使用用户机密,请参阅 此 GitHub 问题

环境变量

环境变量用于避免在代码或本地配置文件中存储应用机密。 环境变量会替代之前指定的所有配置源的配置值。

请考虑使用 ASP.NET Core Web 应用,其中已启用“个人用户帐户”安全性。 默认数据库连接字符串包含在项目的 appsettings.json 文件中,并包含密钥 DefaultConnection。 默认连接字符串适用于 LocalDB,它在用户模式下运行,不需要密码。 在应用部署期间,可使用环境变量的值替代 DefaultConnection 键值。 环境变量可能会存储具有敏感凭据的完整连接字符串。

警告

环境变量通常以未加密纯文本的形式进行存储。 如果计算机或进程遭到入侵,那么不受信任方可访问环境变量。 可能需要采取其他措施来防止泄露用户机密。

所有平台上的环境变量分层键都不支持 : 分隔符。 __(双下划线):

  • 受所有平台支持。 例如,Bash 不支持 : 分隔符,但支持 __
  • 自动替换为 :

机密管理器

机密管理器工具存储 ASP.NET Core 项目开发期间的敏感数据。 在此上下文中,一段敏感数据是应用机密。 应用机密存储在与项目树不同的位置。 应用机密与特定项目关联,或者跨多个项目共享。 应用机密不会签入到源代码管理中。

警告

机密管理器工具不会加密存储的机密,不得被视为受信任的存储。 它仅用于开发。 密钥和值存储在 JS用户配置文件目录中的 ON 配置文件中。

机密管理器工具的工作原理

机密管理器工具会隐藏实现详细信息,例如值的存储位置和存储方法。 可在不知道这些实现详细信息的情况下使用该工具。 这些值存储在 JS本地计算机的用户配置文件文件夹中的 ON 文件中:

文件系统路径:

%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

在上述文件路径中,将 <user_secrets_id> 替换为在项目文件中指定的 UserSecretsId 值。

不要编写依赖于使用机密管理器工具保存的数据的位置或格式的代码。 这些实现详细信息可能有变。 例如,机密值不会加密,但将来可能会加密。

启用机密存储

机密管理器工具会对用户配置文件中存储的特定于项目的配置设置进行操作。

机密管理器工具包含一个 init 命令。 若要使用用户机密,请在项目目录中运行以下命令:

dotnet user-secrets init

上述命令会将 UserSecretsId 元素添加到项目文件的 PropertyGroup 中。 默认情况下,UserSecretsId 的内部文本是 GUID。 内部文本是任意的,但对于项目来说是唯一的。

<PropertyGroup>
  <TargetFramework>netcoreapp3.1</TargetFramework>
  <UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId>
</PropertyGroup>

在 Visual Studio 中,在解决方案资源管理器中右键单击该项目,然后从上下文菜单中选择“管理用户机密”。 该手势会将 UserSecretsId 元素(填充有 GUID)添加到项目文件中。

设置机密

定义由键和值组成的应用机密。 机密与项目的 UserSecretsId 值相关联。 例如,从项目文件所在的目录中运行以下命令:

dotnet user-secrets set "Movies:ServiceApiKey" "12345"

在上述示例中,冒号表示 Movies 是具有 ServiceApiKey 属性的对象文字。

也可从其他目录使用机密管理器工具。 使用 --project 选项提供项目文件所在的文件系统路径。 例如:

dotnet user-secrets set "Movies:ServiceApiKey" "12345" --project "C:\apps\WebApp1\src\WebApp1"

JSVisual Studio 中的 ON 结构平展

Visual Studio 的 “管理用户机密” 手势在文本编辑器中打开一个 secrets.json 文件。 将内容替换为要存储的 secrets.json 键值对。 例如:

{
  "Movies": {
    "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
    "ServiceApiKey": "12345"
  }
}

JS通过或dotnet user-secrets set修改dotnet user-secrets remove后,ON 结构会平展。 例如,运行 dotnet user-secrets remove "Movies:ConnectionString" 会折叠 Movies 对象文字。 修改的文件类似于以下 JSON:

{
  "Movies:ServiceApiKey": "12345"
}

设置多个机密

可以通过将 ON 传递给JSset命令来设置一批机密。 在以下示例中, input.json 文件的内容通过管道传递给 set 命令。

打开命令行界面,然后执行以下命令:

type .\input.json | dotnet user-secrets set

访问机密

若要访问机密,请完成以下步骤:

  1. 注册用户机密配置源
  2. 通过配置 API 读取机密

注册用户机密配置源

用户机密配置提供程序 会向 .NET 配置 API 注册适当的配置源。

通过 dotnet new 或 Visual Studio 创建的 ASP.NET Core Web 应用会生成以下代码:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.CreateBuilder 使用预配置的默认值初始化 类的新实例。 当 EnvironmentNameDevelopment 时,初始化的 WebApplicationBuilder (builder) 会提供默认配置并调用 AddUserSecrets

通过配置 API 读取机密

请考虑以下读取 Movies:ServiceApiKey 键的示例:

Program.cs 文件:

var builder = WebApplication.CreateBuilder(args);
var movieApiKey = builder.Configuration["Movies:ServiceApiKey"];

var app = builder.Build();

app.MapGet("/", () => movieApiKey);

app.Run();

RazorPages 页面模型:

public class IndexModel : PageModel
{
    private readonly IConfiguration _config;

    public IndexModel(IConfiguration config)
    {
        _config = config;
    }

    public void OnGet()
    {
        var moviesApiKey = _config["Movies:ServiceApiKey"];

        // call Movies service with the API key
    }
}

有关详细信息,请参阅 ASP.NET Core 中的配置

将机密映射到 POCO

对于聚合相关属性来说,将整个对象文字映射到 POCO(具有属性的简单 .NET 类)很有用。

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

若要将上述机密映射到 POCO,请使用 .NET 配置 API 的对象图绑定功能。 下面的代码绑定到自定义 MovieSettings POCO 并访问 ServiceApiKey 属性值:

var moviesConfig = 
    Configuration.GetSection("Movies").Get<MovieSettings>();
_moviesApiKey = moviesConfig.ServiceApiKey;

Movies:ConnectionStringMovies:ServiceApiKey 机密映射到 MovieSettings 中的相应属性:

public class MovieSettings
{
    public string ConnectionString { get; set; }

    public string ServiceApiKey { get; set; }
}

用机密替换字符串

以纯文本形式存储密码不太安全。 例如,存储在 appsettings.json 中的数据库连接字符串可能包含指定用户的密码:

{
  "ConnectionStrings": {
    "Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User Id=johndoe;Password=pass123;MultipleActiveResultSets=true"
  }
}

更安全的方法是将密码存储为机密。 例如:

dotnet user-secrets set "DbPassword" "pass123"

appsettings.json 中的连接字符串中删除 Password 键值对。 例如:

{
  "ConnectionStrings": {
    "Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User Id=johndoe;MultipleActiveResultSets=true"
  }
}

可在 SqlConnectionStringBuilder 对象的 Password 属性上设置机密的值来补全连接字符串:

using System.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

var conStrBuilder = new SqlConnectionStringBuilder(
        builder.Configuration.GetConnectionString("Movies"));
conStrBuilder.Password = builder.Configuration["DbPassword"];
var connection = conStrBuilder.ConnectionString;

var app = builder.Build();

app.MapGet("/", () => connection);

app.Run();

列出机密

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

从项目文件所在的目录中运行以下命令:

dotnet user-secrets list

随即显示以下输出:

Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true
Movies:ServiceApiKey = 12345

在前面的示例中,键名称中的冒号表示对象层次结构。secrets.json

删除单个机密

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

从项目文件所在的目录中运行以下命令:

dotnet user-secrets remove "Movies:ConnectionString"

修改应用 secrets.json 的文件以删除与 MoviesConnectionString 键关联的键值对:

{
  "Movies": {
    "ServiceApiKey": "12345"
  }
}

dotnet user-secrets list 显示以下消息:

Movies:ServiceApiKey = 12345

删除所有机密

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

从项目文件所在的目录中运行以下命令:

dotnet user-secrets clear

应用的所有用户机密都已从 secrets.json 文件中删除:

{}

运行 dotnet user-secrets list 将显示以下消息:

No secrets configured for this application.

使用 Visual Studio 管理用户机密

若要在 Visual Studio 中管理用户机密,请在解决方案资源管理器中右键单击该项目,然后选择“管理用户机密”:

显示“管理用户机密”的 Visual Studio

其他资源

作者:Rick AndersonKirk LarkinDaniel RothScott Addie

查看或下载示例代码如何下载

本文档介绍了如何管理开发计算机上 ASP.NET Core 应用的敏感数据。 切勿在源代码中存储密码或其他敏感数据。 不得将生产机密用于开发或测试。 机密不得随应用一起部署。 相反,应通过受控方式(例如环境变量或 Azure Key Vault)访问生产机密。 可使用 Azure Key Vault 配置提供程序存储和保护 Azure 测试和生产机密。

环境变量

环境变量用于避免在代码或本地配置文件中存储应用机密。 环境变量会替代之前指定的所有配置源的配置值。

请考虑使用 ASP.NET Core Web 应用,其中已启用“个人用户帐户”安全性。 默认数据库连接字符串包含在项目的 appsettings.json 文件中,并包含密钥 DefaultConnection。 默认连接字符串适用于 LocalDB,它在用户模式下运行,不需要密码。 在应用部署期间,可使用环境变量的值替代 DefaultConnection 键值。 环境变量可能会存储具有敏感凭据的完整连接字符串。

警告

环境变量通常以未加密纯文本的形式进行存储。 如果计算机或进程遭到入侵,那么不受信任方可访问环境变量。 可能需要采取其他措施来防止泄露用户机密。

所有平台上的环境变量分层键都不支持 : 分隔符。 __(双下划线):

  • 受所有平台支持。 例如,Bash 不支持 : 分隔符,但支持 __
  • 自动替换为 :

机密管理器

机密管理器工具存储 ASP.NET Core 项目开发期间的敏感数据。 在此上下文中,一段敏感数据是应用机密。 应用机密存储在与项目树不同的位置。 应用机密与特定项目关联,或者跨多个项目共享。 应用机密不会签入到源代码管理中。

警告

机密管理器工具不会加密存储的机密,不得被视为受信任的存储。 它仅用于开发。 键和值存储在 JS用户配置文件目录中的 ON 配置文件中。

机密管理器工具的工作原理

机密管理器工具会隐藏实现详细信息,例如值的存储位置和存储方法。 可在不知道这些实现详细信息的情况下使用该工具。 这些值存储在 JS本地计算机的用户配置文件文件夹中的 ON 文件中:

文件系统路径:

%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

在上述文件路径中,将 <user_secrets_id> 替换为在项目文件中指定的 UserSecretsId 值。

不要编写依赖于使用机密管理器工具保存的数据的位置或格式的代码。 这些实现详细信息可能有变。 例如,机密值不会加密,但将来可能会加密。

启用机密存储

机密管理器工具会对用户配置文件中存储的特定于项目的配置设置进行操作。

在 .NET Core SDK 3.0.100 或更高版本中,机密管理器工具包含一个 init 命令。 若要使用用户机密,请在项目目录中运行以下命令:

dotnet user-secrets init

上述命令会将 UserSecretsId 元素添加到项目文件的 PropertyGroup 中。 默认情况下,UserSecretsId 的内部文本是 GUID。 内部文本是任意的,但对于项目来说是唯一的。

<PropertyGroup>
  <TargetFramework>netcoreapp3.1</TargetFramework>
  <UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId>
</PropertyGroup>

在 Visual Studio 中,在解决方案资源管理器中右键单击该项目,然后从上下文菜单中选择“管理用户机密”。 该手势会将 UserSecretsId 元素(填充有 GUID)添加到项目文件中。

设置机密

定义由键和值组成的应用机密。 机密与项目的 UserSecretsId 值相关联。 例如,从项目文件所在的目录中运行以下命令:

dotnet user-secrets set "Movies:ServiceApiKey" "12345"

在上述示例中,冒号表示 Movies 是具有 ServiceApiKey 属性的对象文字。

也可从其他目录使用机密管理器工具。 使用 --project 选项提供项目文件所在的文件系统路径。 例如:

dotnet user-secrets set "Movies:ServiceApiKey" "12345" --project "C:\apps\WebApp1\src\WebApp1"

JSVisual Studio 中的 ON 结构平展

Visual Studio 的 “管理用户机密 ”手势在文本编辑器中打开一个 secrets.json 文件。 将内容替换为要存储的 secrets.json 键值对。 例如:

{
  "Movies": {
    "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
    "ServiceApiKey": "12345"
  }
}

通过JS或修改dotnet user-secrets setdotnet user-secrets remove,ON 结构将平展。 例如,运行 dotnet user-secrets remove "Movies:ConnectionString" 会折叠 Movies 对象文字。 修改的文件类似于以下 JSON:

{
  "Movies:ServiceApiKey": "12345"
}

设置多个机密

可以通过将 JSON set 传递给命令来设置一批机密。 在以下示例中, input.json 文件的内容通过管道传递给命令 set

打开命令行界面,然后执行以下命令:

type .\input.json | dotnet user-secrets set

访问机密

若要访问机密,请完成以下步骤:

  1. 注册用户机密配置源
  2. 通过配置 API 读取机密

注册用户机密配置源

用户机密配置提供程序 会向 .NET 配置 API 注册适当的配置源。

当项目调用 CreateDefaultBuilder 时,用户机密配置源是在开发模式中自动添加的。 当 EnvironmentNameDevelopment 时,CreateDefaultBuilder 会调用 AddUserSecrets

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

如果未调用 CreateDefaultBuilder,则通过在 ConfigureAppConfiguration 中调用 AddUserSecrets 来显式添加用户机密配置源。 只有在开发环境中运行应用时才调用 AddUserSecrets,如以下示例所示:

public class Program
{
    public static void Main(string[] args)
    {
        var host = new HostBuilder()
            .ConfigureAppConfiguration((hostContext, builder) =>
            {
                // Add other providers for JSON, etc.

                if (hostContext.HostingEnvironment.IsDevelopment())
                {
                    builder.AddUserSecrets<Program>();
                }
            })
            .Build();
        
        host.Run();
    }
}

通过配置 API 读取机密

如果注册了用户机密配置源,则 .NET 配置 API 可以读取机密。 构造函数注入可用于获取对 .NET 配置 API 的访问。 请考虑以下读取 Movies:ServiceApiKey 键的示例:

Startup 类:

public class Startup
{
    private string _moviesApiKey = null;

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        _moviesApiKey = Configuration["Movies:ServiceApiKey"];
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Run(async (context) =>
        {
            var result = string.IsNullOrEmpty(_moviesApiKey) ? "Null" : "Not Null";
            await context.Response.WriteAsync($"Secret is {result}");
        });
    }
}

RazorPages 页面模型:

public class IndexModel : PageModel
{
    private readonly IConfiguration _config;

    public IndexModel(IConfiguration config)
    {
        _config = config;
    }

    public void OnGet()
    {
        var moviesApiKey = _config["Movies:ServiceApiKey"];

        // call Movies service with the API key
    }
}

有关详细信息,请参阅 Pages 中的“启动”“访问”配置中的 Razor Access 配置

将机密映射到 POCO

对于聚合相关属性来说,将整个对象文字映射到 POCO(具有属性的简单 .NET 类)很有用。

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

若要将上述机密映射到 POCO,请使用 .NET 配置 API 的对象图绑定功能。 下面的代码绑定到自定义 MovieSettings POCO 并访问 ServiceApiKey 属性值:

var moviesConfig = 
    Configuration.GetSection("Movies").Get<MovieSettings>();
_moviesApiKey = moviesConfig.ServiceApiKey;

Movies:ConnectionStringMovies:ServiceApiKey 机密映射到 MovieSettings 中的相应属性:

public class MovieSettings
{
    public string ConnectionString { get; set; }

    public string ServiceApiKey { get; set; }
}

用机密替换字符串

以纯文本形式存储密码不太安全。 例如,存储在 appsettings.json 中的数据库连接字符串可能包含指定用户的密码:

{
  "ConnectionStrings": {
    "Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User Id=johndoe;Password=pass123;MultipleActiveResultSets=true"
  }
}

更安全的方法是将密码存储为机密。 例如:

dotnet user-secrets set "DbPassword" "pass123"

appsettings.json 中的连接字符串中删除 Password 键值对。 例如:

{
  "ConnectionStrings": {
    "Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User Id=johndoe;MultipleActiveResultSets=true"
  }
}

可在 SqlConnectionStringBuilder 对象的 Password 属性上设置机密的值来补全连接字符串:

public class Startup
{
    private string _connection = null;

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var builder = new SqlConnectionStringBuilder(
            Configuration.GetConnectionString("Movies"));
        builder.Password = Configuration["DbPassword"];
        _connection = builder.ConnectionString;

        // code omitted for brevity
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Run(async (context) =>
        {
            await context.Response.WriteAsync($"DB Connection: {_connection}");
        });
    }
}

列出机密

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

从项目文件所在的目录中运行以下命令:

dotnet user-secrets list

随即显示以下输出:

Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true
Movies:ServiceApiKey = 12345

在前面的示例中,键名称中的冒号表示对象层次结构。secrets.json

删除单个机密

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

从项目文件所在的目录中运行以下命令:

dotnet user-secrets remove "Movies:ConnectionString"

修改应用 secrets.json 的文件以删除与 MoviesConnectionString 键关联的键值对:

{
  "Movies": {
    "ServiceApiKey": "12345"
  }
}

dotnet user-secrets list 显示以下消息:

Movies:ServiceApiKey = 12345

删除所有机密

假设应用 secrets.json 的文件包含以下两个机密:

{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}

从项目文件所在的目录中运行以下命令:

dotnet user-secrets clear

应用的所有用户机密都已从 secrets.json 文件中删除:

{}

运行 dotnet user-secrets list 将显示以下消息:

No secrets configured for this application.

使用 Visual Studio 管理用户机密

若要在 Visual Studio 中管理用户机密,请在解决方案资源管理器中右键单击该项目,然后选择“管理用户机密”:

显示“管理用户机密”的 Visual Studio

其他资源