发布于 2026-01-06 0 阅读
0

安全存储密码(C#)

安全存储密码(C#)

介绍

学习如何使用 NuGet 包BCrypt.Net-NextMicrosoft 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 项目,包含两个页面,均使用哈希密码进行登录。这两个页面的唯一区别在于,一个页面提供密码显示切换功能,而另一个页面则没有。

用于登录的 ASP.NET Core 网页

设置

<ItemGroup>
   <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
   <Using Include="BCrypt.Net.BCrypt" Alias="BC" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

为了保持代码简洁,本项目仅针对数据库中的单个用户进行操作,该用户是在名为 的控制台项目中创建和填充的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();
    }
}
Enter fullscreen mode Exit fullscreen mode

关键在于在 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();
}
Enter fullscreen mode Exit fullscreen mode

注意:
如果不使用 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);
}
Enter fullscreen mode Exit fullscreen mode

接下来,该类会验证密码。

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

将以下代码添加到 Program.cs 文件中。

builder.Services.AddScoped<IAuthentication, Authentication>();
Enter fullscreen mode Exit fullscreen mode

登录页面

主构造函数用于 DbContext 和身份验证工作。

public class IndexModel(Context context, IAuthentication authentication) : PageModel
Enter fullscreen mode Exit fullscreen mode

模拟用户的属性。

[BindProperty]
public User CurrentUser { get; set; }
Enter fullscreen mode Exit fullscreen mode

用于指示成功或失败的属性。当然,也可以使用对话框。

public string Message { get; set; } = "";
Enter fullscreen mode Exit fullscreen mode

前端有一个带有事件处理程序的按钮。

<button type="submit" class="btn btn-primary mb-3" asp-page-handler="ValidateUser">
    Login
</button>
Enter fullscreen mode Exit fullscreen mode

上述按钮的 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";
}
Enter fullscreen mode Exit fullscreen mode

请注意,这是表配置,对于此示例读取一个模拟用户而言,不需要 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);

}
Enter fullscreen mode Exit fullscreen mode

添加更多用户

在项目 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"
  }
]
Enter fullscreen mode Exit fullscreen mode

参见

概括

根据提供的代码示例,完全没有理由将明文密码存储在数据库中。

文章来源:https://dev.to/karenpayneoregon/storing-passwords-safely-c-ifh