安全存储密码(C#)
介绍
学习如何使用 NuGet 包BCrypt.Net-Next和Microsoft EF Core以及值转换器,轻松地将密码存储在 SQL Server 数据库表中。
许多新手开发者会将密码存储在文本文件或数据库表中,这使得密码很容易被窥探。密码绝不应该以明文形式存储,而应该进行哈希处理。
注意:
本文介绍的方法也适用于其他数据库,例如 SQLite、Oracle、PostgreSQL 等。此外,数据操作并非仅限于 EF Core,基本操作也可以使用 Dapper 等工具完成。
重要的
还有更安全的密码保护方法。即使攻击者获得了密码哈希值,这里介绍的方法仍然可以用来尝试暴力破解。因此,对于像亚马逊这样的大型企业或银行来说,这里介绍的方法无法满足其密码安全需求。
BCrypt.Net-Next相比数据库表中明文密码等没有任何安全保障的做法,是一个很好的开端。
对于 ASP.NET Core,一个不错的选择是PasswordHasher 上的PasswordHasher<TUser> Class一篇很棒的文章《探索 ASP.NET Core Identity PasswordHasher》,这篇文章甚至微软也引用了它。
项目
| 项目名称 | 描述 |
|---|---|
| BCrypt.Net-Next | |
| 管理员应用程序 | 用于创建模拟用户 |
| DapperSample | 潇洒的例子 |
| EF_Core_ValueConversionsEncryptProperty | 创建数据库 |
| 哈希无数据库应用程序 | 密码哈希无需数据库 |
| RazorPages示例 | ASP.NET Core 示例 |
| 其他 | |
| 生成密码应用程序 | 获取随机/安全密码的示例 |
| NetDevPackHasherArgonApp | 使用 NetDevPack.Security.PasswordHasher.Argon2 NuGet 包 |
需要
需要Microsoft Visual Studio 2022或更高版本,并支持 .NET Core 8。其他编辑器/IDE(例如 Rider 和 Microsoft VS-Code)也可以使用,但 Windows Forms 项目除外。
表格结构
基本结构包含足够的列来显示密码哈希值、主键、用户名和密码。根据业务需求,可能需要更多列。
EF电动工具
使用EF Power Tools Visual Studio 扩展对数据库进行了逆向工程,是的,没有使用迁移。
示例项目
该项目是一个 ASP.NET Core 项目,包含两个页面,均使用哈希密码进行登录。这两个页面的唯一区别在于,一个页面提供密码显示切换功能,而另一个页面则没有。
设置
- 安装 NuGet 包BCrypt.Net-Next
- 安装 NuGet 包Microsoft.EntityFrameworkCore.SqlServer
- 将以下包别名添加到项目文件中。
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<Using Include="BCrypt.Net.BCrypt" Alias="BC" />
</ItemGroup>
为了保持代码简洁,本项目仅针对数据库中的单个用户进行操作,该用户是在名为 的控制台项目中创建和填充的EF_Core_ValueConversionsEncryptProperty。
运行此项目将使用 EF Core 来:
- 创建数据库,如果数据库已存在,则会重新创建。
- 使用正确的明文密码验证新条目是否有效。
- NotAuthenticateUser 验证新条目是否能使用错误的明文密码正常工作
代码
internal partial class Program
{
private static async Task Main(string[] args)
{
await Examples.CreateDatabase();
Console.WriteLine();
await Examples.AuthenticateUser();
Console.WriteLine();
await Examples.NotAuthenticateUser();
ExitPrompt();
}
}
关键在于在 OnModelCreating 中使用值转换器,如下所示。
- HasConversion 函数的第一部分将给定的明文密码哈希到数据库表中。
- HasConversion 函数的第二部分从表中检索哈希密码。
public class Context : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => BC.HashPassword(v),
v => v);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(new DbContextToFileLogger().Log,
[DbLoggerCategory.Database.Command.Name],
LogLevel.Information)
.UseSqlServer(ConnectionString())
.EnableSensitiveDataLogging();
}
注意:
如果不使用 ASP.NET Core,上面提供的代码应该足以对密码进行哈希处理。如果不使用 EF Core,Dapper 或许可以直接使用 BC.HashPassword(纯文本密码)。
回到 ASP.NET Core 项目。
对于依赖注入,添加以下接口。
/// <summary>
/// Defines methods for authenticating users within the application.
/// </summary>
public interface IAuthentication
{
/// <summary>
/// Validates the specified user against the provided context.
/// </summary>
/// <param name="user">The user to validate, containing user credentials.</param>
/// <param name="context">The database context used to retrieve user information.</param>
/// <returns>
/// A tuple where the first element indicates whether the user is valid,
/// and the second element is the user's ID if validation is successful, or -1 if not.
/// </returns>
(bool, int) ValidateUser(User user, Context context);
}
接下来,该类会验证密码。
public class Authentication : IAuthentication
{
/// <summary>
/// Validates the specified user by comparing the provided password with the stored password in the database.
/// </summary>
/// <param name="user">The user object containing the credentials to validate.</param>
/// <param name="context">The database context used to access the stored user data.</param>
/// <returns>
/// A tuple containing a boolean and an integer:
/// <list type="bullet">
/// <item>
/// <description><c>true</c> if the provided password matches the stored password for the user; otherwise, <c>false</c>.</description>
/// </item>
/// <item>
/// <description>The user's ID if the password matches; otherwise, -1.</description>
/// </item>
/// </list>
/// </returns>
/// <remarks>
/// This method utilizes the BCrypt library to verify the password and logs the result of the authentication attempt.
/// </remarks>
public (bool, int) ValidateUser(User user, Context context)
{
var current = context.Users.FirstOrDefault(x => x.Name == user.Name);
return current is null ?
(false, -1) :
(BC.Verify(user.Password, current.Password), current.Id);
}
}
将以下代码添加到 Program.cs 文件中。
builder.Services.AddScoped<IAuthentication, Authentication>();
登录页面
主构造函数用于 DbContext 和身份验证工作。
public class IndexModel(Context context, IAuthentication authentication) : PageModel
模拟用户的属性。
[BindProperty]
public User CurrentUser { get; set; }
用于指示成功或失败的属性。当然,也可以使用对话框。
public string Message { get; set; } = "";
前端有一个带有事件处理程序的按钮。
<button type="submit" class="btn btn-primary mb-3" asp-page-handler="ValidateUser">
Login
</button>
上述按钮的 OnPost 事件执行验证并使用 SeriLog 显示结果,同时设置 Bootstrap 5.3 警报的文本@Html.Raw(Model.Message)。
public void OnPostValidateUser()
{
var (authenticated, id) = authentication.ValidateUser(CurrentUser!, context);
Log.Information(authenticated ?
"{Id,-3} User {Name} authenticated" :
"User {Name} not authenticated", id,CurrentUser.Name);
Message = authenticated ? "Authenticated" : "Not authenticated";
}
请注意,这是表配置,对于此示例读取一个模拟用户而言,不需要 HasConversion,但对于接受新用户的实际应用程序来说,则需要 HasConversion。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RazorPagesSample.Models;
namespace RazorPagesSample.Data.Configurations;
public partial class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> entity)
{
entity.ToTable("User");
entity.Property(e => e.Name).IsRequired();
entity.Property(e => e.Password).IsRequired();
entity.Property(e => e.Password).HasConversion(
v => BC.HashPassword(v),
v => v);
OnConfigurePartial(entity);
}
partial void OnConfigurePartial(EntityTypeBuilder<User> entity);
}
添加更多用户
在项目 AdminApplication(Windows 窗体)中,使用 NuGet 包Bogus以低成本的方式生成用户。
- 保留第一条记录
- 如果数据库不存在,应用程序将停止并显示通知,然后正常结束。
为了获取用于应用程序的明文密码,需要UsersExposed.json在应用程序文件夹中创建这些密码。
[
{
"Id": 2,
"Name": "Arthur_Anderson37",
"Password": "wee98uD_Yj"
},
{
"Id": 3,
"Name": "Allen51",
"Password": "CwjTmtAVwz"
},
{
"Id": 4,
"Name": "Inez_Skiles26",
"Password": "3p0jAJh8IV"
},
{
"Id": 5,
"Name": "Marlon.Kreiger1",
"Password": "LMa_iXHiMW"
},
{
"Id": 6,
"Name": "Cody_Davis",
"Password": "z4_qWZWs7p"
}
]
参见
概括
根据提供的代码示例,完全没有理由将明文密码存储在数据库中。
文章来源:https://dev.to/karenpayneoregon/storing-passwords-safely-c-ifh


