使用 ASP.NET Core 安全地存储开发中的应用机密
作者:Rick Anderson 和 Kirk 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"
}
}
通过 JSdotnet user-secrets remove
或 dotnet user-secrets set
进行修改后,ON 结构会平展。 例如,运行 dotnet user-secrets remove "Movies:ConnectionString"
会折叠 Movies
对象文字。 修改后的文件类似于以下 JSON:
{
"Movies:ServiceApiKey": "12345"
}
设置多个机密
可以通过将管道 JSON 传递给 命令来 set
设置一批机密。 在以下示例中 input.json
,文件的内容通过管道传递给 set
命令。
打开命令行界面,然后执行以下命令:
type .\input.json | dotnet user-secrets set
访问机密
若要访问机密,请完成以下步骤:
注册用户机密配置源
用户机密配置提供程序 会向 .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 使用预配置的默认值初始化 类的新实例。 当 EnvironmentName 是 Development 时,初始化的 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:ConnectionString
和 Movies: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
应用的文件已修改为删除与键关联的Movies:ConnectionString
键值对:
{
"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 中管理用户机密,请在解决方案资源管理器中右键单击该项目,然后选择“管理用户机密”:
将用户机密从 ASP.NET Framework 迁移到 ASP.NET Core
请参阅此 GitHub 问题。
其他资源
- 若要了解如何从 IIS 访问用户机密,请查看此问题。
- ASP.NET Core 中的配置
- ASP.NET Core 中的 Azure Key Vault 配置提供程序
作者:Rick Anderson、Kirk Larkin、Daniel Roth 和 Scott 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"
}
}
通过 JSdotnet user-secrets remove
或 dotnet user-secrets set
进行修改后,将平展 ON 结构。 例如,运行 dotnet user-secrets remove "Movies:ConnectionString"
会折叠 Movies
对象文字。 修改后的文件类似于以下 JSON:
{
"Movies:ServiceApiKey": "12345"
}
设置多个机密
可以通过将 ON 管道 JS传递给 命令来 set
设置一批机密。 在以下示例中,文件 input.json
的内容通过管道传递给 set
命令。
打开命令行界面,然后执行以下命令:
type .\input.json | dotnet user-secrets set
访问机密
若要访问机密,请完成以下步骤:
注册用户机密配置源
用户机密配置提供程序 会向 .NET 配置 API 注册适当的配置源。
当项目调用 CreateDefaultBuilder 时,用户机密配置源是在开发模式中自动添加的。 当 EnvironmentName 是 Development 时,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 访问配置。
将机密映射到 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:ConnectionString
和 Movies: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 中管理用户机密,请在解决方案资源管理器中右键单击该项目,然后选择“管理用户机密”:
将用户机密从 ASP.NET Framework 迁移到 ASP.NET Core
请参阅此 GitHub 问题。
其他资源
- 若要了解如何从 IIS 访问用户机密,请查看此问题。
- ASP.NET Core 中的配置
- ASP.NET Core 中的 Azure Key Vault 配置提供程序