カスタムユーザとの5つの関連付けられた



イントロ
今回はカスタムユーザーと署名してみます.
デフォルトのASPです.ネットコアアイデンティティのユーザーはとても多くのプロパティを持っているし、彼らはあまりにも私のためです.
  • ID
  • ユーザー名
  • 電子メール
  • パスワッシュ
  • メール確認
  • NormalizedUserName
  • 正規化メール
  • 低気圧の
  • アクセス失敗カウント
  • 音素
  • コンカレント
  • セキュリティ
  • lockotend
  • 二項分解
  • 音韻
  • 私は最初の4つを使用します.
    これを行うには、何かしなければならない.

    環境
  • Nlogウェブアスペルタルver .4.10.0
  • NPGSQL.EntityFrameworkCore.PostgreSQLバージョン.5.0.2
  • マイクロソフト.EntityFrameworkCoreバージョン.5.0.2
  • マイクロソフト.EntityFrameworkCore.デザインバージョン.5.0.2
  • Newtonsoft.JSONバージョン.12.0.3
  • マイクロソフト.アスピネット.MVCNewtonsoftJSONバージョン5.0.2
  • マイクロソフト.アスピネット.アイデンティティ.エンティティ

  • サンプル
  • ApproveWorkflowSample

  • カスタムユーザーの作成
    カスタムユーザーを作成するには、“IdentityUser”を継承します.
    そして私は“組織”と“lastupdatedate”を追加しました.

    アプリケーションユーザー.cs
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using Microsoft.AspNetCore.Identity;
    
    namespace ApprovementWorkflowSample.Applications
    {
        // To use int as ID type, I inherited "IdentityUser<int>".
        public class ApplicationUser: IdentityUser<int>
        {
            [Key]
            [Column("id")]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public override int Id { get; set; }
            [Required]
            [Column("user_name")]
            public override string UserName { get; set; } = "";
            [Column("organization")]
            public string? Organization { get; set; }
            [Required]
            [Column("mail")]
            public override string Email { get; set; } = "";
            [Required]
            [Column("password")]
            public override string PasswordHash { get; set; } = "";
            [Required]
            [Column("last_update_date", TypeName = "timestamp with time zone")]
            public DateTime LastUpdateDate { get; set; }
            [NotMapped]
            public override bool EmailConfirmed { get; set; }
            [NotMapped]
            public override string NormalizedUserName {
                get
                {
                    return UserName.ToUpper();
                }
                set { /* DO nothing*/ }
            }
            [NotMapped]
            public override string NormalizedEmail {
                get
                {
                    return Email.ToUpper();
                }
                set { /* DO nothing*/ }
            }
            [NotMapped]
            public override bool LockoutEnabled { get; set; }
            [NotMapped]
            public override int AccessFailedCount { get; set; }
            [NotMapped]
            public override string? PhoneNumber { get; set; }
            [NotMapped]
            public override string? ConcurrencyStamp { get; set; }
            [NotMapped]
            public override string? SecurityStamp { get; set; }
            [NotMapped]
            public override DateTimeOffset? LockoutEnd { get; set; }
            [NotMapped]
            public override bool TwoFactorEnabled { get; set; }
            [NotMapped]
            public override bool PhoneNumberConfirmed { get; set; }     
            public void Update(ApplicationUser user)
            {
                UserName = user.UserName;
                Organization = user.Organization;
                Email = user.Email;
                PasswordHash = user.PasswordHash;
            }
            public void Update(string userName, string? organization,
                string email, string password)
            {
                UserName = userName;
                Organization = organization;
                Email = email;
                // set hashed password text to PasswordHash.
                PasswordHash = new PasswordHasher<ApplicationUser>()
                    .HashPassword(this, password);
            }
            public string Validate()
            {
                if(string.IsNullOrEmpty(UserName))
                {
                    return "UserName is required";
                }
                if(string.IsNullOrEmpty(Email))
                {
                    return "E-Mail address is required";
                }
                if(string.IsNullOrEmpty(PasswordHash))
                {
                    return "Password is required";
                }
                return "";
            }
        }
    }
    
    私はデータベースを作成し、このような“ApplicationUser”テーブルを追加します.


  • ユニークな制約
    デフォルトでは、usernameは一意であるように制限されます.
    このサンプルでは、メールを制限する必要もあります.

    ワークフローコンテキスト.cs
    using ApprovementWorkflowSample.Applications;
    using Microsoft.EntityFrameworkCore;
    
    namespace ApprovementWorkflowSample.Models
    {
        public class ApprovementWorkflowContext: DbContext
        {
    ...
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<ApplicationUser>()
                    .Property(w => w.LastUpdateDate)
                    .HasDefaultValueSql("CURRENT_TIMESTAMP");
                modelBuilder.Entity<ApplicationUser>()
                    .HasIndex(u => u.Email)
                    .IsUnique();                
            }
            public DbSet<ApplicationUser> ApplicationUsers => Set<ApplicationUser>();
        }
    }
    

    役割
    今回は、私は特別な役割を追加しません.
    だからデフォルトの“IdentityPost”を使います.

    起動.cs
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    ...
    namespace ApprovementWorkflowSample
    {
        public class Startup
        {
    ...
            public void ConfigureServices(IServiceCollection services)
            {
    ...
                services.AddDbContext<ApprovementWorkflowContext>(options =>
                    options.UseNpgsql(configuration["DbConnection"]));
                // THIS PROJECT CAUSES AN ERROR
                services.AddIdentity<ApplicationUser, IdentityRole<int>>()
                    .AddEntityFrameworkStores<ApprovementWorkflowContext>()
                    .AddDefaultTokenProviders();
    ...
            }
    
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                app.UseStaticFiles();
                app.UseRouting();
    
                app.UseAuthentication();
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                });
            }
        }
    }
    

    アイデンティティー
    "IdentityUser < int >"を使うときは"IdentityPost < int >"を使わなければなりません.
    または、実行時に例外を取得します.
    Stopped program because of exception System.ArgumentException: GenericArguments[1], 'Microsoft.AspNetCore.Identity.IdentityRole', on 'Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`4[TUser,TRole,TContext,TKey]' violates the constraint of type 'TRole'.
     ---> System.TypeLoadException: GenericArguments[1], 'Microsoft.AspNetCore.Identity.IdentityRole', on 'Microsoft.AspNetCore.Identity.UserStoreBase`8[TUser,TRole,TKey,TUserClaim,TUserRole,TUserLogin,TUserToken,TRoleClaim]' violates the constraint of type parameter 'TRole'.
    ...
    

    カスタムユーザー
    このプロジェクトには問題がある.
    「ApplicationUser」のいくつかのプロパティはNULLです.
    NullReferenceExceptionを回避するには、カスタムユーザーストアを追加する必要があります.
    パスワードでサインするので、「iuserpasswordstore」を実装します.

    ApplicationUserStorecs
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using ApprovementWorkflowSample.Models;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Storage;
    using Microsoft.Extensions.Logging;
    
    namespace ApprovementWorkflowSample.Applications
    {
        public class ApplicationUserStore: IUserPasswordStore<ApplicationUser>
        {
            private readonly ILogger<ApplicationUserStore> logger;
            private readonly ApprovementWorkflowContext context;
            public ApplicationUserStore(ILogger<ApplicationUserStore> logger,
                ApprovementWorkflowContext context)
            {
                this.logger = logger;
                this.context = context;
            }
            public async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                // validation
                string validationError = user.Validate();
                if(string.IsNullOrEmpty(validationError) == false)
                {
                    return IdentityResult.Failed(new IdentityError { Description = validationError });
                }
                using(IDbContextTransaction transaction = context.Database.BeginTransaction())
                {
                    if(await context.ApplicationUsers
                        .AnyAsync(u => u.Email == user.Email,
                        cancellationToken))
                    {
                        return IdentityResult.Failed(new IdentityError { Description = "Your e-mail address is already used" });
                    }
                    var newUser = new ApplicationUser();
                    newUser.Update(user);
                    await context.ApplicationUsers.AddAsync(newUser, cancellationToken);
                    await context.SaveChangesAsync(cancellationToken);
                    await transaction.CommitAsync();
                    return IdentityResult.Success;
                }
            }
            public async Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                ApplicationUser? target = await context.ApplicationUsers
                    .FirstOrDefaultAsync(u => u.Id == user.Id,
                    cancellationToken); 
                context.ApplicationUsers.Remove(target);
                await context.SaveChangesAsync(cancellationToken);
                return IdentityResult.Success;
            }
            public void Dispose() { /* do nothing */ }
            public async Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
            {
                if(int.TryParse(userId, out var id) == false)
                {
                    return new ApplicationUser();
                }
                return await context.ApplicationUsers
                    .FirstOrDefaultAsync(u => u.Id == id,
                    cancellationToken);
            }
            public async Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
            {
                return await context.ApplicationUsers
                    .FirstOrDefaultAsync(u => u.UserName.ToUpper() == normalizedUserName,
                    cancellationToken);
            }
            public async Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                return await Task.FromResult(user.NormalizedUserName);
            }
            public async Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                return await Task.FromResult(user.PasswordHash);
            }
            public async Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                return await Task.FromResult(user.Id.ToString());
            }
            public async Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                return await Task.FromResult(user.UserName);
            }
            public async Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                return await Task.FromResult(true);
            }
            public async Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken)
            {
                // do nothing
                await Task.Run(() => {});
            }
            public async Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken)
            {
                using(IDbContextTransaction transaction = context.Database.BeginTransaction())
                {
                    ApplicationUser? target = await context.ApplicationUsers
                        .FirstOrDefaultAsync(u => u.Id == user.Id,
                        cancellationToken);
                    target.PasswordHash = passwordHash;
                    // validation
                    await context.SaveChangesAsync();
                    await transaction.CommitAsync();
                }            
            }
            public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
            {
                using(IDbContextTransaction transaction = context.Database.BeginTransaction())
                {
                    ApplicationUser? target = await context.ApplicationUsers
                        .FirstOrDefaultAsync(u => u.Id == user.Id,
                        cancellationToken);
                    target.UserName = userName;
                    // validation
                    await context.SaveChangesAsync();
                    await transaction.CommitAsync();
                }
            }
            public async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
            {
                string validationError = user.Validate();
                if(string.IsNullOrEmpty(validationError) == false)
                {
                    return IdentityResult.Failed(new IdentityError { Description = validationError });
                }
                using(IDbContextTransaction transaction = context.Database.BeginTransaction())
                {
                    ApplicationUser? target = await context.ApplicationUsers
                        .FirstOrDefaultAsync(u => u.Id == user.Id,
                        cancellationToken);
                    // validation
                    target.Update(user);
                    await context.SaveChangesAsync(cancellationToken);
                    await transaction.CommitAsync();
                    return IdentityResult.Success;
                }
            }
        }
    }
    

    起動.cs
    ...
    namespace ApprovementWorkflowSample
    {
        public class Startup
        {
    ...
            public void ConfigureServices(IServiceCollection services)
            {
    ...
                // OK
                services.AddIdentity<ApplicationUser, IdentityRole<int>>()
                    .AddUserStore<ApplicationUserStore>()
                    .AddEntityFrameworkStores<ApprovementWorkflowContext>()
                    .AddDefaultTokenProviders();
    ...
    

    資源
  • Introduction to Identity on ASP.NET Core | Microsoft Docs
  • ASP.NET Identityカスタマイズに挑戦 - かずきのBlog@hatena

  • サインマネージャー

    サインとサインアウト
    ログインしてサインをすることができます.

    ApplicationUserService.cs
    using System.Security.Claims;
    using System;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Logging;
    
    namespace ApprovementWorkflowSample.Applications
    {
        public class ApplicationUserService: IApplicationUserService
        {
            private readonly ILogger<ApplicationUsers> logger;
            private readonly IApplicationUsers applicationUsers;
            private readonly SignInManager<ApplicationUser> signInManager;
    
            public ApplicationUserService(ILogger<ApplicationUsers> logger,
                IApplicationUsers applicationUsers,
                SignInManager<ApplicationUser> signInManager)
            {
                this.logger = logger;
                this.applicationUsers = applicationUsers;
                this.signInManager = signInManager;
            }
            public async Task<bool> SignInAsync(string email, string password)
            {
                var target = await applicationUsers.GetByEmailAsync(email);
                if (target == null)
                {
                    return false;
                }
                var result = await signInManager.PasswordSignInAsync(target, password, false, false);
                return result.Succeeded;
            }
            public async Task SignOutAsync()
            {
                await signInManager.SignOutAsync();
            }        
        }
    }
    
    "passwordsigninasync "は"username "を使ってサインするだけです.
    それで、電子メールでサインしたいなら、上記のように最初にメールでユーザーを見つけなければなりません.
  • asp.net mvc - How to login using email in identity 2? - Stack Overflow

  • ユーザーの作成
    "signinmanager "もユーザを作成することができます.

    ApplicationUserService.cs
    ...
    namespace ApprovementWorkflowSample.Applications
    {
        public class ApplicationUserService: IApplicationUserService
        {
    ...
            public async Task<IdentityResult> CreateAsync(string userName, string? organization, string email, string password)
            {
                var newUser = new ApplicationUser();
                newUser.Update(userName, organization, email, password);
                return await signInManager.UserManager.CreateAsync(newUser);
            }
    ...
    

    ユーザ情報の署名を取得する
    サインをした後に、httpcontextからユーザー情報を得ることができます.

    ApplicationUserService.cs
    using System.Security.Claims;
    using System;
    using System.Threading.Tasks;
    using ApprovementWorkflowSample.Applications.Dto;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Logging;
    
    namespace ApprovementWorkflowSample.Applications
    {
        public class ApplicationUserService: IApplicationUserService
        {
    ...
            private readonly SignInManager<ApplicationUser> signInManager;
            private readonly IHttpContextAccessor httpContextAccessor;
    
            public ApplicationUserService(ILogger<ApplicationUsers> logger,
                IApplicationUsers applicationUsers,
                SignInManager<ApplicationUser> signInManager,
                IHttpContextAccessor httpContextAccessor)
            {
    ...
                this.signInManager = signInManager;
                this.httpContextAccessor = httpContextAccessor;
            }
    ...
            public async ValueTask<User?> GetSignInUserAsync()
            {
                ClaimsPrincipal? user = httpContextAccessor.HttpContext?.User;
                if (user == null)
                {
                    return null;
                }
                if(signInManager.IsSignedIn(user) == false)
                {
                    return null;
                }
                string? userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
                if(string.IsNullOrEmpty(userId) ||
                    int.TryParse(userId, out var id) == false)
                {
                    return null;
                }
                ApplicationUser? appUser = await applicationUsers.GetByIdAsync(id);
                if (appUser == null)
                {
                    return null;
                }
                return new User(appUser.Id, appUser.UserName, appUser.Organization, appUser.Email);
            }
    ...