ASP.NETCore标识、身份验证和数据库引擎解耦

71 篇文章 3 订阅

目录

介绍

背景

使用代码

Microsoft.AspNetCore.Identity Namespace中的类的扩展

Microsoft.AspNetCore.Identity.EntityFrameworkCore命名空间中的类的扩展

用于存储身份信息、身份验证和角色的数据库

支持同一用户登录多个设备和浏览器选项卡

ASP.NET安全启动代码

建立对身份验证数据库的访问

设置JWT持有者令牌身份验证

使应用与数据库引擎无关

集成测试

兴趣点


介绍

如果您决定在ASP.NET Core应用程序中托管自己的身份验证机制,那么有许多优秀的文章演示了如何使用ASP.NET Core IdentityOAuthJWT令牌。假设您在使用ASP.NET Core Identity方面有一些经验。本文重点介绍ASP.NET Core身份、身份验证和数据库引擎之间的解耦。

如果您不熟悉OAuthJWTGitHub存储库中包含的集成测试套件演示了使用OAuth的常见身份验证对话。

这是一篇简短的文章,不会涵盖构建复杂业务应用程序的每个方面和每个级别的安全问题,但是,提供的提示基于一些假设/上下文:

  1. 您正在构建一个绿地项目。
  2. 绿地项目中的身份验证机制将由将来使用的其他业务应用程序共享。
  3. 一些应用程序将使oAuth与Google,Microsoft,Apple和Facebook等,以及其他专有身份验证。
  4. 某些前端应用程序支持Push,实现为SignalR。
  5. 实体(包括用户实体)的首选ID类型是GUID而不是字符串。
  6. Entity Framework Core用于生成DAL
  7. 该应用程序对数据库引擎是中立的。也就是说,您可以轻松切换到MySQL,SQLite,MS SQL和Oracle等。
  8. 您希望同一用户John Smith在多个设备上保持登录状态,并且每个设备都有多个应用程序或多个浏览器选项卡。

ASP.NET应用的要求可能有很大不同,但是,通过以下代码示例描述的概念可能会大量应用。

引用

背景

如果在ASP.NET Core之前从未使用过 ASP.NET Identity,则可以跳过此背景部分,了解.NET Framework中提供的功能。

带有.NET FrameworkVisual Studio为没有经验的Web应用程序开发人员提供了相当容易的起点。创建新的ASP.NET MVC项目时,将具有这样的结构和基架代码:

Web.config

<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-WebApplication1-20240328045515.mdf;Initial Catalog=aspnet-WebApplication1-20240328045515;Integrated Security=True"
      providerName="System.Data.SqlClient" />
  </connectionStrings>
  
  ....
  
   <entityFramework>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
...

您可以在生成或部署后切换到其他数据库引擎,因为运行时将在应用启动期间读取连接字符串和EF提供程序的配置。

对于企业应用程序,这种规定的结构可能有几个主要缺陷:

  • 对于单元测试和集成测试来说并不容易。为了分离关注点,您可能会将文件夹“Models”中的内容和其他内容移动到另一个package/assembly/ csproj。上帝集合没有错,就像上帝类没有错一样,但是,对TDD不友好,因此对构建企业应用程序没有效率。
  • SQL数据库需要需要root或DBA角色的连接字符串,这对于像Orchard和Umbraco这样的CMS应用程序来说还不错,但对于常见的业务应用程序来说,就不好了。
  • 整个架构针对MS SQL进行了优化/耦合/偏向于MS SQL。
  • ...

自从OAuth流行以来,多年来从头开始设计ASP.NET Core时,MicrosoftMS SQL以外的数据库引擎变得更加友好,并且对于可能没有深入OAuth经验的应用程序开发人员来说,利用ASP.NET Core IdentityOwin以及Entity Framework Core变得更加容易。

ASP.NET Core项目的基架代码用于身份验证和帐户管理较少,这对企业应用程序来说可能是一件好事,因为:

  • 还有更多的身份验证方案,以及Auth0和okta等第三方授权提供商。有时,您、IT团队或安全团队可能希望更改身份验证和授权机制,那么您肯定希望迁移尽可能顺利。ASP.NET Core为这些需求提供了相当优雅的建筑设计。

使用代码

如果要托管自己的用户标识和授权,则希望使用Microsoft.AspNetCore.Identity命名空间和Microsoft.AspNetCore.Identity.EntityFrameworkCore命名空间中的类提供的内容。

代码示例主要介绍以下主题:

  1. 将ID类型从字符串更改为GUID,并使用更多列扩展某些数据库表。
  2. 将数据库引擎与主要应用程序代码分离。也就是说,您可以在部署后切换数据库引擎。
  3. 集成测试套件,用于验证实施的身份验证是否实际有效。

Microsoft.AspNetCore.Identity Namespace中的类的扩展

这是为了在Microsoft.AspNetCore.Identity命名空间中扩展类。

命名空间包含IdentityRole<TKey >IdentityUser<TKey>等泛型类以及用于标识模型的内置具体类如IdentityRole:IdentityRole<string>IdentityUser:IdentityUser<string>

如果使用内置的具体类IdentityRole或类似类,则Id的类型将是DB模式“VARCHAR(255)”的字符串。对于大多数应用程序来说,这样的数据库模式显然是一个安全的选择。但是,如果您确定uuid/GUID在长度方面足够好,并且更喜欢像GUID这样的强类型ID,则可以创建一个具体的类,例如:

public class ApplicationIdentityRole : IdentityRole<Guid>
{
    public ApplicationIdentityRole()
    {
        Id = Guid.NewGuid();
    }

    public ApplicationIdentityRole(string roleName) : this()
    {
        Name = roleName;
    }
}

public class ApplicationUser : IdentityUser<Guid>, ITrackableEntity
{
    /// <summary>
    /// Full name of the user. And this could be used as a filter or logical foreight key with a user.
    /// </summary>
    [MaxLength(128)]
    public string FullName { get; set; }

    public DateTime CreatedUtc { get; set; }

    public DateTime ModifiedUtc { get; set; }
}

public class ApplicationUserToken : IdentityUserToken<Guid>, INewEntity
{
    public DateTime CreatedUtc { get; set; }
}

public interface INewEntity{
    DateTime CreatedUtc { get; set; }
}

对于用户配置文件,您可能需要跟踪创建日期和修改日期,以及显示的全名。

提示

  • 即使你只使用 IdentityRole:IdentityRole<string>和 IdentityUser:IdentityUser<string>或它们的派生类来制作标识模型,本文中介绍的技巧可能会启发你进行对TDD和未来扩展和扩展友好的解耦组件设计。使用字符串作为ID的基本类型可以很好地处理需要处理遗留ID和多个唯一ID方案的遗留或棕地项目。
  • 典型的业务应用程序可能有一个小型CRM,其中可能包含内部用户和外部用户的信息,并且身份验证数据库的用户配置文件中的全名可能与CRM中的全名不同。
  • 即使通过向类“ApplicationUser”添加更多属性可以很容易地向表“aspnetusers”添加更多列,但在此表中存储用户的个人信息并不好,因为:
    • 身份验证的属性很少更改,而个人信息的属性可能会随时间而更改。
    • 个人信息的更新可能会更频繁。
    • 您希望身份验证数据库运行得非常快,而个人信息的CURD可能会相对较慢。
    • ...

Microsoft.AspNetCore.Identity.EntityFrameworkCore命名空间中的类的扩展

此命名空间包含一些泛型类,用于在标识模型上创建DbContext架构和数据库架构。

public class ApplicationRoleStore : RoleStore<ApplicationIdentityRole, ApplicationDbContext, Guid>
{
    public ApplicationRoleStore(ApplicationDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}

public class ApplicationUserStore : UserStore<ApplicationUser, ApplicationIdentityRole, ApplicationDbContext, Guid>
{
    public ApplicationUserStore(ApplicationDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationIdentityRole, Guid>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
                : base(options)
    {
    }

    /// <summary>
    /// make table aspnetuserroles visible in context
    /// </summary>
    public override DbSet<IdentityUserRole<Guid>> UserRoles { get; set; }

    /// <summary>
    /// For shorter key length of MySQL
    /// </summary>
    /// <param name="modelBuilder"></param>
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.UseCollation("utf8_general_ci"); //case insensitive
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationIdentityRole>()
            .Property(c => c.Name).HasMaxLength(128).IsRequired();

        modelBuilder.Entity<ApplicationUser>()//.ToTable("AspNetUsers")//I have to declare the table name, otherwise IdentityUser will be created
            .Property(c => c.UserName).HasMaxLength(128).IsRequired();
    }

...

public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(IUserStore<ApplicationUser> store,
        IOptions<IdentityOptions> optionsAccessor,
        IPasswordHasher<ApplicationUser> passwordHasher,
        IEnumerable<IUserValidator<ApplicationUser>> userValidators,
        IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
        ILookupNormalizer keyNormalizer,
        IdentityErrorDescriber errors,
        IServiceProvider services,
        ILogger<UserManager<ApplicationUser>> logger)
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
    }


...

用于存储身份信息、身份验证和角色的数据库

通过上面的2组扩展,您已经自定义了与内置模型略有不同的EF模型。根据您的具体需求,如果默认架构不够,您可以添加更多信息。使用ASP.NETCore)标识几乎要求实体框架(Core)的代码优先方法。

public async Task DropAndCreate()
{
    using ApplicationDbContext context = new(options);
    if (await context.Database.EnsureDeletedAsync())
    {
        Console.WriteLine("Old db is deleted.");
    }

    await context.Database.EnsureCreatedAsync();
    Console.WriteLine(String.Format("Database is initialized, created,  or altered through connection string: {0}", context.Database.GetDbConnection().ConnectionString));
}

提示

  • 通常,我会调用一个控制台应用程序或脚本的DropAndCreate()来创建空白数据库,通常使用一些预定义的角色和管理员帐户。这类似于通过具有DBA权限的SQL脚本创建数据库,并在具有CRUD权限的应用程序中使用数据库的良好旧做法。

支持同一用户登录多个设备和浏览器选项卡

由于JWT是无状态的,因此ASP.NET Core Identity以及Microsoft.AspNetCore.Authorization支持同一用户登录多个设备和浏览器选项卡,除非:

  • 访问令牌的到期时间很短,例如5分钟或1小时等。
  • 使用刷新令牌。

应用程序开发人员通常使用IdentityUserToken(在表aspnetusertokens中)来存储刷新令牌,这是一种正统的方式。但是,表aspnetusertokensIdentityUserToken设计使用UserId+LoginProvider+Name作为主键。虽然从技术上讲,更改主键的组成很容易,但是,UserManager用于用户令牌的内置函数固有地使用UserId+LoginProvider+Name作为主键,因此,更改主键将破坏UserManager

我想出了一个有点笨拙的解决方案,如下所述:

public class UserTokenHelper
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="userManager"></param>
    /// <param name="tokenProviderName">Your app token provider name, or oAuth2 token provider name.</param>
    public UserTokenHelper(ApplicationUserManager userManager, string tokenProviderName)
    {
        this.userManager = userManager;
        this.tokenProviderName = tokenProviderName;
    }

    readonly ApplicationUserManager userManager;

    readonly string tokenProviderName;
    /// <summary>
    /// Add or update a token of an existing connection.
    /// </summary>
    /// <returns></returns>
    public async Task<IdentityResult> UpsertToken(ApplicationUser user, string tokenName, string newTokenValue, Guid connectionId)
    {
        string composedTokenName = $"{tokenName}_{connectionId.ToString("N")}";
        await userManager.RemoveAuthenticationTokenAsync(user, tokenProviderName, composedTokenName); // need to remove it first, otherwise, Set won't work. Apparently by design the record is immutable.
        return await userManager.SetAuthenticationTokenAsync(user, tokenProviderName, composedTokenName, newTokenValue);
    }

    /// <summary>
    /// Lookup user tokens and find 
    /// </summary>
    /// <returns></returns>
    public async Task<bool> MatchToken(ApplicationUser user, string tokenName, string tokenValue, Guid connectionId)
    {
        string composedTokenName = $"{tokenName}_{connectionId.ToString("N")}";
        string storedToken = await userManager.GetAuthenticationTokenAsync(user, tokenProviderName, composedTokenName);
        return tokenValue == storedToken;
    }
}

诀窍和技巧是使令牌名称成为基本令牌名称加上连接ID

用户令牌的各种内务管理功能可以很容易地实现。

评论:

  • 在代码示例中,我使用“UserManager.SetAuthenticationTokenAsync()”来更新“aspnetusertokens”表,而不是使用Entity Framework Core,因为这更抽象、更灵活。如果将其他custom存储提供程序用于ASP.NET Core Identity,则所需的代码更改将是最少的。
  • 在数据库表中存储刷新令牌显然不是唯一的方法,因为我看到其他程序员(在线帖子)使用了一对“UserManager.GenerateUserTokenAsync()”和“userManager.VerifyUserTokenAsync()”。显然,不需要数据库操作,但是,刷新令牌不会过期。
  • 您不必使用刷新令牌的概念,如果您的IT安全专家根据您的业务上下文达成一致,那么在令牌到期之前通过HTTP客户端拦截器通过用户凭据获取新的访问令牌可能就足够了。您可能有一些使用某些在线服务的经验,这些服务会在几分钟不活动后将您注销,显然他们并没有故意使用刷新令牌的概念。

ASP.NET安全启动代码

建立对身份验证数据库的访问

builder.Services.AddDbContext<ApplicationDbContext>(dcob =>
{
    ConfigApplicationDbContext(dcob);
});

builder.Services.AddIdentity<ApplicationUser, ApplicationIdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddUserManager<ApplicationUserManager>()
                .AddDefaultTokenProviders()
                .AddTokenProvider(authSettings.TokenProviderName, typeof(DataProtectorTokenProvider<ApplicationUser>));

设置JWT持有者令牌身份验证

builder.Services.AddAuthentication(
    options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }
).AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidAudience = authSettings.Audience,
        ValidIssuer = authSettings.Issuer,
        IssuerSigningKey = issuerSigningKey,
    }; 
});

IssuerSigningKey的值在生产过程中应得到很好的保护,并且该值是由一个常量生成的,该常量应安全地存储以供生产使用。在本文中,使用纯文本字符串:

var issuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(authSetupSettings.SymmetricSecurityKeyString));
builder.Services.AddSingleton(issuerSigningKey);

讨论如何为生产存储这样的秘密超出了本文的范围,但有很多很好的参考资料:

使应用与数据库引擎无关

这个想法是在appsetting.json中更改连接字符串和相应的插件,就像ASP.NET能够做到的那样。

服务启动代码中:

var dbEngineDbContext = DbEngineDbContextLoader.CreateDbEngineDbContextFromAssemblyFile(dbEngineDbContextPlugins[0] + ".dll");
if (dbEngineDbContext == null)
{
    Console.Error.WriteLine("No dbEngineDbContext");
    throw new ArgumentException("Need dbEngineDbContextPlugin");
}

Console.WriteLine($"DB Engine: {dbEngineDbContext.DbEngineName}");

builder.Services.AddDbContext<ApplicationDbContext>(dcob =>
{
    dbEngineDbContext.ConnectDatabase(dcob, identityConnectionString); // called by runtime everytime an instance of ApplicationDbContext is created.
});

SQLite插件代码中

namespace Fonlow.EntityFrameworkCore.Sqlite
{
    public class SqliteDbEngineDbContext : Fonlow.EntityFrameworkCore.Abstract.IDbEngineDbContext
    {
        public string DbEngineName => "Sqlite";

        public void ConnectDatabase(DbContextOptionsBuilder dcob, string connectionString)
        {
            dcob.UseSqlite(connectionString);
        }
    }
}

appsettings.json

"ConnectionStrings": {
    "IdentityConnection": "Data Source=./DemoApp_Data/auth.db"
},

"appSettings": {
    "environment": "test",
    "dbEngineDbContextPlugins": [ "Fonlow.EntityFrameworkCore.Sqlite" ]
},

集成测试

身份验证是用户在典型业务应用程序中使用的第一个功能,因此,确保身份验证的以下质量属性对于整体用户体验和营销至关重要:

  • 正确性
  • 可靠性
  • 鲁棒性
  • 速度

当然,其他业务功能也应该具有这些质量属性,但是,对于身份验证,额外的努力更值得。

GitHub上的 AuthTests代码

[Fact]
public void TestRefreshTokenWithNewHttpClient()
{
    var tokenText = GetTokenWithNewClient(baseUri, "admin", "Pppppp*8");
    Assert.NotEmpty(tokenText);

    var tokenModel = System.Text.Json.JsonSerializer.Deserialize<TokenResponseModel>(tokenText);
    Assert.NotNull(tokenModel.RefreshToken);

    var newTokenModel = GetTokenResponseModelByRefreshTokenWithNewClient(baseUri, tokenModel.RefreshToken, tokenModel.Username, tokenModel.ConnectionId);
    Assert.Equal(tokenModel.Username, newTokenModel.Username);
    TestAuthorizedConnection(newTokenModel.TokenType, newTokenModel.AccessToken);
}

测试套件将启动“Core3WebApi”并检索JWT访问令牌和刷新令牌。为了方便测试,Web服务具有以下设置:

  • JWT访问令牌将在5秒后过期。
  • 时钟偏差为2秒,因此访问令牌将在5+2=7秒后过期

您应该能够签出存储库、构建和运行测试,因为数据库引擎默认为SQLite

提示

  1. 通过测试套件,可以看到使用刷新令牌获取新的访问令牌的速度比登录快10倍左右。除了性能之外,这还减少了通过网络发送客户端凭据的需要。
  2. 调整测试套件的设置,您可以在团队测试环境和暂存环境中测试身份验证系统的性能。

兴趣点

“Introduction to Identity on ASP.NET Core”中,Microsoft建议:

ASP.NET Core IdentityASP.NET Core Web应用添加了用户界面(UI)登录功能。若要保护Web APISPA,请使用以下方法之一:

但是,ASP.NET Identity 2ASP.NET Core Identity都支持良好的SPA,这些SPA只是在Web浏览器上运行的独立客户端,否则,这些SPA如何在这些拯救生命的身份验证提供商出现之前幸存下来?

如果您将来决定使用Auth0/OktaMicrosoft Entra Id作为身份验证提供程序,只要您的ASP.NET应用程序的架构设计符合ASP.NET Core的架构设计,迁移就相当容易和直接。如果您有兴趣使用Entra,只需在谷歌上搜索“ASP.NET Core AddAuthentication Entra”,并确保查看2024-01-01之后编写的代码示例。

毕竟,请咨询IT团队中的安全专家,尤其是当您拥有内部软件应用程序以及一些Microsoft Office 365产品等时,并且人们希望在授权中获得无缝的用户体验。

如果你是Web API供应商,并且提供各种形式的API密钥来保护API是相当简单的。在不久的将来,我可能会写一篇关于这些的文章。

如果监视auth数据库中的aspnet*表,您可能会注意到登录期间很少有表刷新令牌和授权,并且唯一更新的表是“aspnetusertokens”。显然,ASP.NETCore)身份的安全库将访问令牌存储在某些秘密地方甚至无效中。

延伸阅读:

  • Access Token和Refresh Token :通俗易懂,用于解释这2个令牌背后的设计背景,作者是Roman Imankulov、catchdave和laalaguer。

https://www.codeproject.com/Articles/5378824/Decouple-ASP-NET-Core-Identity-Authentication-and

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值