.net core 2.x - ids4 - identity - two factory 登錄認證


本片內容使用到ids4+ids4.Entityframework持久化表單,以及core的identity相關表的一並持久化,然后就是登錄認證,認證使用email發送郵件的方式。所以這里涉及到四塊內容,1.ids4的集成,2.ids4+core identity的相關默認表的持久化,以及在遷移庫、表的過程中初始化相關數據(用戶數據);3.登錄認證 4.mailkit郵件發送(見上篇),框架是按照ddd搭建的,該篇內容只是ddd中的一個支撐域的東西(一個子域),用於統一認證和授權的,內容比較簡單也比較少,但是框架沒完全寫好,所以不放出來了。

core 2.x項目集成ids4

1.首先需要創建一個.net core項目,然后

2.選擇使用登錄認證(這也就涉及到了identity的東西了),然后創建好項目之后

3.右擊解決方案,打開解決方案文件夾,

4.按住shift,然后右擊鼠標,點擊 powerShell,輸入以下內容回車:iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/master/getmaster.ps1'))

此時可以看到項目中生成了 quictstart.ui的相關內容,此內容是ids4的參考ui,相對於自己寫省了不少事情了,詳見:https://github.com/IdentityServer/IdentityServer4.Quickstart.UI 或者自己直接百度 identityserver4 ui就可以搜到。

 

持久化ids4 和 identity的相關表單


1.首先,我們在使用ids4的時候,需要添加兩個遷移文件,詳見這里:http://docs.identityserver.io/en/latest/quickstarts/7_entity_framework.html

dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb

以上兩航依舊是在上面的步驟中的 powershell執行的。

2.然后是ids的相關配置(startup.cs中),configureServices方法

services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            //services.AddDbContext<ApplicationDbContext>(options =>
            //    options.UseSqlServer(
            //        Configuration.GetConnectionString("DefaultConnection")));
            //services.AddDefaultIdentity<IdentityUser>()
            //    .AddDefaultUI(UIFramework.Bootstrap4)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();

            //services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
            services.AddIdentity<ApplicationUser/*IdentityUser*/, ApplicationRole>(options =>
            {
                // Password settings
                options.Password.RequireDigit = false;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireLowercase = false;
                options.Password.RequiredUniqueChars = 2;
                // Lockout settings
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
                options.Lockout.MaxFailedAccessAttempts = 5;
                options.Lockout.AllowedForNewUsers = true;
                // Signin settings
                options.SignIn.RequireConfirmedEmail = false;
                options.SignIn.RequireConfirmedPhoneNumber = false;
                // User settings
                options.User.RequireUniqueEmail = false;
            })
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            //添加ids4配置
            services.AddIdentityServer()
                 .AddDeveloperSigningCredential()
                 //.AddSigningCredential(new X509Certificate2(@"D:\WORKSPACE\CSHARP_CORE\esoftor-ddd\src\ESoftor.Authorization.Server\bin\Debug\netcoreapp2.2\tempkey.rsa"))
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                        builder.UseSqlServer(connectionString,
                            sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = builder =>
                        builder.UseSqlServer(connectionString,
                            sql => sql.MigrationsAssembly(migrationsAssembly));
                    options.EnableTokenCleanup = true;
                    options.TokenCleanupInterval = 30;
                })
                .AddAspNetIdentity<ApplicationUser/*IdentityUser*/>();

configure方法中

 app.UseCookiePolicy();

            //app.UseAuthentication();
            app.UseIdentityServer();//ids4的UseIdentityServer包含了UseAuthentication,所以不需要上面的UseAuthentication

3.最重要的一步,因為添加完上面的東西之后會報錯,呵呵,nuget添加以下相關的 程序集:

IdentityServer4

IdentityServer4.AccessTokenValidation

IdentityServer4.AspNetIdentity

IdentityServer4.EntityFramework

因為需要遷移生成庫,所以還需要添加ef core的相關包

Microsoft.EntityFrameworkCore.SqlServer (這里我是用的mssql)

Microsoft.EntityFrameworkCore.Tools

此時我們的基礎工作基本完成了,其中涉及到ApplicationUser ApplicationRole,ApplicationUserRole,ApplicationIdentityUserLogin的內容,如下:

// ApplicationIdentityUserLogin.cs
public class ApplicationIdentityUserLogin : IdentityUserLogin<Guid>
    {
    }
// ApplicationRole.cs
 public class ApplicationRole : IdentityRole<Guid>
    {
        /// <summary>
        /// Gets or sets the UserRoles
        /// </summary>
        public ICollection<ApplicationUserRole> UserRoles { get; set; }
    }
// ApplicationUser.cs
public class ApplicationUser : IdentityUser<Guid>
    {
        /// <summary>
        /// Gets or sets the UserRoles
        /// </summary>
        public ICollection<ApplicationUserRole> UserRoles { get; set; }
    }
// ApplicationUserRole 
public class ApplicationUserRole : IdentityUserRole<Guid>
    {
        /// <summary>
        /// Gets or sets the User
        /// </summary>
        public virtual ApplicationUser User { get; set; }

        /// <summary>
        /// Gets or sets the Role
        /// </summary>
        public virtual ApplicationRole Role { get; set; }
    }

然后我們需要針對core 的identity,注意,這里說的是identity,再單獨生成一個遷移文件,這里就不說了,無非是 add-migration或者 dotnet ef add migrations 

4.依舊在startup.cs中添加遷移用方法,說之前先說下ids4的官網, http://docs.identityserver.io/en/latest/quickstarts/7_entity_framework.html,其中提供的參考代碼就是我們需要的,但是我們這里還需要添加對identity的相關表數據的初始化,也就是我們上面定義的幾個表ApplicationUser,App....,所以我們的代碼如下:

private void InitializeDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
            {
                //ids4
                serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

                var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
                context.Database.Migrate();
                if (!context.Clients.Any())
                {
                    foreach (var client in InMemoryConfiguration.Clients())
                    {
                        context.Clients.Add(client.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.IdentityResources.Any())
                {
                    foreach (var resource in InMemoryConfiguration.GetIdentityResources())
                    {
                        context.IdentityResources.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.ApiResources.Any())
                {
                    foreach (var resource in InMemoryConfiguration.ApiResources())
                    {
                        context.ApiResources.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }

                //aspNet Identity
                serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
                var appContext = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
                appContext.Database.Migrate();
                if (!appContext.Roles.Any())
                {
                    foreach (var item in ApplicationDbContextDataSeed.Roles)
                    {
                        appContext.Roles.Add(item);
                    }
                    appContext.SaveChanges();
                }

                if (!appContext.Users.Any())
                {
                    foreach (var item in ApplicationDbContextDataSeed.Users)
                    {
                        appContext.Users.Add(item);
                    }
                    appContext.SaveChanges();
                }

                if (!appContext.UserRoles.Any())
                {
                    foreach (var item in ApplicationDbContextDataSeed.UserRoles)
                    {
                        appContext.UserRoles.Add(item);
                    }
                    appContext.SaveChanges();
                }

            }
        }

  這時候只需要在 configure方法中調用即可 

這時候我們只需要直接運行項目就額可以生成了 ids4的相關表,以及identity的幾個表了,如下圖:

 

 

登錄認證

基於ids4的默認的登陸方法,我們修改如下(applicationController中):首先要注入

private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;

private readonly ILogger _logger;

修改登錄方法如下:

/// <summary>
        /// Handle postback from username/password login
        /// </summary>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
            // check if we are in the context of an authorization request
            var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);

            if (ModelState.IsValid)
            {
                var user = await _userManager.FindByNameAsync(model.Username);
                if (user == null)
                {
                    await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
                    ModelState.AddModelError(string.Empty, AccountOptions.InvalidUsername);
                }

                // validate username/password against in-memory store
                var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberLogin, lockoutOnFailure: false);

                if (result.Succeeded)
                {
                    AuthenticationProperties props = null;
                    if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                    {
                        props = new AuthenticationProperties
                        {
                            IsPersistent = true,
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                        };
                    };

                    if (context != null)
                    {
                        if (await _clientStore.IsPkceClientAsync(context.ClientId))
                        {
                            return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                        }

                        return Redirect(model.ReturnUrl);
                    }

                    // request for a local page
                    if (Url.IsLocalUrl(model.ReturnUrl))
                    {
                        return Redirect(model.ReturnUrl);
                    }
                    else if (string.IsNullOrEmpty(model.ReturnUrl))
                    {
                        return Redirect("~/");
                    }
                    else
                    {
                        // user might have clicked on a malicious link - should be logged
                        throw new Exception("invalid return URL");
                    }
                }

                if (result.RequiresTwoFactor)
                {
                    //return RedirectToAction(nameof(LoginWith2fa), new { model.ReturnUrl, model.RememberLogin });
                    return RedirectToAction(nameof(SendCode), new { model.ReturnUrl, model.RememberLogin });
                }
                if (result.IsLockedOut)
                {
                    return RedirectToAction(nameof(Lockout));
                }
                else
                {
                    //ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
                    ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
                    //return View(model);
                }
            }

            // something went wrong, show form with error
            var vm = await BuildLoginViewModelAsync(model);
            return View(vm);
        }

  其中涉及到一個 RequiresTwoFactor 的 二次認證的方法,SendCode,也就是我們鋪墊了這么久要說的對象了,方法如下:

/// <summary>
        ///     發送驗證碼頁面
        /// </summary>
        /// <param name="returnUrl"></param>
        /// <returns></returns>
        [HttpGet]
        [AllowAnonymous]
        public async Task<ActionResult> SendCode(string returnUrl, bool rememberMe)
        {
            var userId = await _signInManager.GetTwoFactorAuthenticationUserAsync();
            if (userId == null)
            {
                return View("Error");
            }

            //假設默認Email 獲取方式進行驗證//生成二次驗證的 token
            var twoFactoryToken = await _userManager.GenerateTwoFactorTokenAsync(userId, "Email");
            //發送email
            EmailHelper.SendMail(new EmailInfo()
            {
                From = new System.Collections.Generic.List<EmailAddress>() { new EmailAddress("esoftor's framework(esoftor-from)", "1365101128@qq.com") },
                To = new System.Collections.Generic.List<EmailAddress>() { new EmailAddress("esoftor's framework(esoftor-to)", "1365101128@qq.com") },
                Subject = "esoftor's core 2.x framework 登錄驗證碼",
                HtmlBody = $"<div style='font-size:18;font-weight:bold'>您正在進行esoftor's core 2.x framework 的二次認證授權登錄,您的驗證碼為:{twoFactoryToken}</div>"
            });
            //二次驗證方式 email,phone?...
            var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(userId);
            var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
            SelectList selectLists = new SelectList(factorOptions);
            return View(new SendCodeViewModel
            {
                Providers = selectLists,
                ReturnUrl = returnUrl
            });
        }

  

  對應的 cshtml頁面如下:

@model ESoftor.Authorization.Server.Models.SendCodeViewModel

@{
    ViewData["Title"] = "SendCode";
}

<h1>SendCode</h1>

<h4>SendCodeViewModel</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="SendCode">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="ReturnUrl" class="control-label"></label>
                <input asp-for="ReturnUrl" class="form-control" />
                <span asp-validation-for="ReturnUrl" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="TwoFactoryToken" class="control-label"></label>
                <input asp-for="TwoFactoryToken" class="form-control" />
                <span asp-validation-for="TwoFactoryToken" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Providers" class="control-label"></label>
                @*@Html.DropDownList("Providers", Model.Providers, new { @class = "form-control custom-select" })*@
                <select class="form-control custom-select">
                    @foreach (SelectListItem item in Model.Providers.Items)
                    {
                        <option value="@item.Value">@item.Text</option>
                    }
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

  當我們點擊這里的 create的按鈕的時候,就會提交到后台的驗證碼驗證方法,如下:

[HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> SendCode(SendCodeViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View();
            }
            var userId = await _signInManager.GetTwoFactorAuthenticationUserAsync();
            //// Generate the token and send it
            //if (!await _userManager.SendTwoFactorCodeAsync(model.SelectedProvider))
            //{
            //    return View("Error");
            //}
            var twoFactorProviders = await _userManager.GetValidTwoFactorProvidersAsync(userId);
            //return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl });
            //生成二次驗證的 token
            //var twoFactoryToken = _userManager.GenerateTwoFactorTokenAsync(userId, model.Providers.Where(x => x.Selected).First().Value);
            //驗證Email的token(code)
            var validTwoToken = await _userManager.VerifyTwoFactorTokenAsync(userId, "Email", model.TwoFactoryToken);
            if (validTwoToken)
            {
                var twoSignInResult = await _signInManager.TwoFactorSignInAsync("Email", model.TwoFactoryToken, isPersistent: true, rememberClient: false);
                if (twoSignInResult.Succeeded)
                    return Redirect(model.ReturnUrl);
            }

            ModelState.AddModelError(string.Empty, "Invalid Two Factory code.");
            return View();
        }

  

  到這里就完成了,代碼中哦都有注釋,若干是不清楚,可以留言。這里稍微需要注意的就是 core的identity也就是上面注入的 UserManager和SignInManager的兩個方法,和以前的owin不同,所以你搜到到的很多資料是驢唇不對馬嘴的,也就是上面加紅的部分,core的identity api中變成了以上的命名方法,如果搜到不一致,不要驚訝,因為我們這里是core。

 參考圖如下:

項目跑起來之后,登錄中之前,

 

 

登錄之后獲取core二次認證,此時可以收到登錄的短信或者郵件通知,內容包含了登陸所需的驗證碼,同時頁面變為輸入驗證碼的頁面(右圖)

 

 

 

認證成功登錄中之后,提示獲取授權信息:

 

 完。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM