手頭一個小應用項目,非常非常小。。。
主要功能也就是提供一個網頁頁面瀏覽數據,數據內容部分在數據庫中,圖像部分在阿里OSS對象存儲中;
最最前期采用ASP.NET WebForm做過一些WEB應用開發,再后來學習過ASP.NET MVC4,一直也沒有機會實戰;
通過近期的學習考慮,准備本次小應用直接通過ASP.NET Core最新版 3.1 來架構,雖然有點大馬拉小車,但也是個難得的實踐機會;
碰到的第一個問題就是,小應用雖小,但也是一個內部應用項目,並不是對外部用戶全開放的,需要為用戶配置登錄賬號,用戶通過賬號登錄后才可以進行頁面瀏覽查詢;
以前了解過MemberShip 和MVC4 框架下的Identity ,總體來說,當時總覺得好像邏輯好復雜,自定義部分很不順(當然也可能是沒深入研究的原因);
而完全自己開發一個網頁應用的論證登錄框架好像有點太過了,目前還沒那能力,個人還是以開發實際功能應用為主的,並不是框架開發高手;
所以,本次應用准備采用ASP.NET Core 3.1自帶的Identity 做用戶管理部分框架,深入學習Identity ,並考慮一些簡單擴展(例如:管理員對用戶的管理、批量導入新建等等)
准備環境: VS2019 版本號:16.6.2 , ASP.NET Core 版本 3.1
學習一:研究默認解決方案自帶個人認證
第一步:新建一個帶個人認證的ASP.NET Core Web應用程序
項目命名為 MyCoreTest1
選擇 Web應用程序(MVC) (該架構也可以加入Restful WEB API的內容,比較符合我的應用要求)
然后在右側身份驗證點擊更改 -> 選擇 個人用戶賬戶 -> 選擇默認的存儲應用內的用戶賬戶 最后點擊 確認按鈕:
注:選擇了身份驗證后,會自動只能選擇HTTPS,后面會有幾個窗口需要點確定(好像是和SSL證書相關的)
最后點創建按鈕,就自動把一個項目建好了:
直接跑起來看看吧,按F5 或者 Ctrl+F5:
標題欄右側 有兩個按鈕,分別為 Register 和 Login,既然是全新的應用,目前肯定沒用戶,那就點Register按鈕試試看:
默認Identity框架用email 作為用戶賬號,密碼有一定的安全策略(必須超過6位、必須帶大寫字符、小寫字符、數字、特殊字符)
輸入賬號: test@test.com ,密碼 P@ssw0rd ,然后點 Register按鈕,出現一個數據庫錯誤;
當然是因為還沒有建數據庫的原因,直接點擊中間的 Apply Migrations 來讓應用自動創建數據庫;
按鈕旁邊出現: Try refreshing the page ,則表示數據庫創建好了,(后面會提到數據庫創建到哪里去了。),
按F5刷新頁面,會進入 注冊確認 頁面,默認框架對於用於注冊必須有一個確認動作,主要是預留給有 郵件確認需求的;(即用來確認用戶是否真的擁有這個郵箱)
如果不在這個頁面點擊 Click here to confirm your account,會發生能登記賬號,但不能登錄的情況:
點擊后,出現 Confirm email 信息,表示用戶已確認:
這個時候點擊右上角 Login : 輸入剛才登記的用戶信息: (test@test.com , 密碼:P@ssw0rd)
登錄后,右上角出現 注冊用戶email ,則表示登錄成功:
哇,真的很神奇,一句代碼沒寫,一個基本用戶注冊、登錄功能全實現了!
現在,查看一下數據庫被建在哪里了,以及為啥會被建到那里,打開 視圖 --> SQL Server 對象資源管理器
原來數據庫被建在Localdb 里了:
打開后,可以看到已經建立如下表:
打開AspNetUsers 表可以看到里面剛才新建的一個用戶:
去查看一下項目中的 appsettings.json 配置文件,其中定義了數據庫連接字符串 (DefaultConnection):
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-MyCoreTest1-3DF9EC28-CCC2-495F-AF13-FE6E503EC9F6;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
同時在Startup.cs中有一段代碼定義使用這個字符串:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddControllersWithViews(); services.AddRazorPages(); }
可根據需要修改字符串名 及 數據庫的字符串值 來連接自己希望的數據庫;
先再建個新的空數據庫,起名:MyCoreTestDB1
在appsettings.json 配置文件增加一個名為 MyConnection 連接字符串:
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-MyCoreTest1-3DF9EC28-CCC2-495F-AF13-FE6E503EC9F6;Trusted_Connection=True;MultipleActiveResultSets=true", "MyConnection": "Server=(localdb)\\mssqllocaldb;Database=MyCoreTestDB1;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("MyConnection"))); services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddControllersWithViews(); services.AddRazorPages(); }
然后再跑起來看看,點擊 Login
就出現了數據庫表不存在的錯誤,繼續點擊 Apply Migrations 后再刷新,顯示 Invalid Login attempt (當然啦,數據庫是新的啦,沒有用戶)
按上面注冊用戶步驟,完成用戶注冊及用戶確認,然后再次登錄:
默認建好的帶個人認證的解決方案,目錄結構如下:
發現一個問題,那些 注冊用戶、登錄頁面 代碼在哪里呢? (登錄界面還能看到有個View: _LoginPartial.cshtml ,注冊用戶頁面直接啥也沒有)
在 Identity 的 Areas 里只有一個Page: _ViewStart.cshtml。。。
我只能猜測應該是封裝后,編譯到DLL里了。
那么問題來了,如果我覺得這些默認頁面不是我想要的,或者說上面一些信息我需要修改一下,怎么辦?
右鍵點擊 項目: MyCoreTest1 然后點擊 添加 --> 新搭建基架的項目
左側選擇 標識 ,然后中間選擇 標識 (這NB的翻譯。。。)
會自動搭建基架 ,安裝一些NuGet包:
彈出了,可以重載的內容: (翻譯成替代,我也真服了)
這次我就先重載 注冊頁 和 登錄頁,然后數據上下文類也先用原本的: (會發現沒辦法點用戶類旁邊的加號,也即不能用自己定義的用戶類,后面再說怎么用自己定義的用戶類)
點 添加后,VS自動開始加載代碼:
首先會彈出一個ReadMe: 這個鏈接可以去看看: https://go.microsoft.com/fwlink/?linkid=2116645
資源管理器里可以看到Areas -> Identity -> Pages 下加入了Account 目錄, 有了Login.cshtml 和 Register.cshtml
雙擊 Login.cshtml會發現 這個不是一個原來理解的View ,而是 ASP.NET Core 新的一種類似於原來aspx的開發方式: Razor Pages
在此代碼基礎上就可以進行修改來實現自己的一些要求或設想;
首先,我目前還不考慮第三方外部認證方式,直接就把頁面上相關顯示的DIV刪除:
@page @model LoginModel @{ ViewData["Title"] = "Log in"; } <h1>@ViewData["Title"]</h1> <div class="row"> <div class="col-md-4"> <section> <form id="account" method="post"> <h4>Use a local account to log in.</h4> <hr /> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> <span asp-validation-for="Input.Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.Password"></label> <input asp-for="Input.Password" class="form-control" /> <span asp-validation-for="Input.Password" class="text-danger"></span> </div> <div class="form-group"> <div class="checkbox"> <label asp-for="Input.RememberMe"> <input asp-for="Input.RememberMe" /> @Html.DisplayNameFor(m => m.Input.RememberMe) </label> </div> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Log in</button> </div> <div class="form-group"> <p> <a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a> </p> <p> <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a> </p> <p> <a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a> </p> </div> </form> </section> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
好了,頁面清爽一點了:
下一步,我想用戶注冊不需要強制用Email注冊
打開 Register.cshtml.cs 在 InputModel中增加一個UserName 字段,必須輸入、長度1-20,顯示名為 賬號:
同時修改 OnPostAsync 方法中一個賦值,將UserName直接賦值給UserName,而不是把email賦值給UserName;
public class InputModel { [Required] [StringLength(20, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)] [Display(Name = "賬號")] public string UserName { get; set; } [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } }
public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { var user = new IdentityUser { UserName = Input.UserName, Email = Input.Email }; var result = await _userManager.CreateAsync(user, Input.Password); //.........省略其他下面的代碼 }
修改 Register.cshtml 增加一項輸入,並且刪除 第三方外部用戶注冊部分的DIV:
@page @model RegisterModel @{ ViewData["Title"] = "Register"; } <h1>@ViewData["Title"]</h1> <div class="row"> <div class="col-md-4"> <form asp-route-returnUrl="@Model.ReturnUrl" method="post"> <h4>Create a new account.</h4> <hr /> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.UserName"></label> <input asp-for="Input.UserName" class="form-control" /> <span asp-validation-for="Input.UserName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> <span asp-validation-for="Input.Email" class="text-danger"></span> </div> //....省略下面一些代碼 </form> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
跑起來后,可以采用賬號 + Email + 密碼 來注冊賬號,(這里沒有把Email的強制輸入關閉,主要是后面還有個確認頁面是傳送email過去的,先留着這個強制了)
但是發現這個用戶雖然注冊成功,但是在登錄頁面無法登錄,email也不行,賬號也不行。。。
通過查看Login頁面的OnPostAsync 方法中驗證用戶的函數調用,才發現是硬代碼把 email輸入當做 username來驗證了,是因為原先的框架 username 等於 email
知道原因就好修改了,先修改InputModel:
public class InputModel { [Required] [Display(Name = "賬號")] public string UserName { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } }
var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
再修改一下頁面:
@page @model LoginModel @{ ViewData["Title"] = "Log in"; } <h1>@ViewData["Title"]</h1> <div class="row"> <div class="col-md-4"> <section> <form id="account" method="post"> <h4>Use a local account to log in.</h4> <hr /> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.UserName"></label> <input asp-for="Input.UserName" class="form-control" /> <span asp-validation-for="Input.UserName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.Password"></label> <input asp-for="Input.Password" class="form-control" /> <span asp-validation-for="Input.Password" class="text-danger"></span> </div> //.....省略下面一些代碼 </form> </section> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
搞定,用testuser賬號登錄:
下一步,想修改一下取消email確認,(至於完善email確認,在后期再補充考慮),並且在用戶注冊后,直接自動Login 進入;
注釋掉 和郵件確認相關的代碼,並且把新建一個IdentityUser中直接硬編碼 EmailConfirmed = true
再把SignInAsync 第2個參數改為 True,即創建完用戶就直接登錄:
public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); //ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { var user = new IdentityUser { UserName = Input.UserName, Email = Input.Email, EmailConfirmed = true }; var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { //_logger.LogInformation("User created a new account with password."); //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); //code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); //var callbackUrl = Url.Page( // "/Account/ConfirmEmail", // pageHandler: null, // values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl }, // protocol: Request.Scheme); //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", // $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>."); //if (_userManager.Options.SignIn.RequireConfirmedAccount) //{ // return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); //} //else //{ await _signInManager.SignInAsync(user, isPersistent: true); return LocalRedirect(returnUrl); //} } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } }
新建后,直接就登錄了:
在一開始沒有全部重載Identity的文件情況下,如果還需對其他文件進行重載,可以繼續 右鍵點擊 項目: MyCoreTest1 然后點擊 添加 --> 新搭建基架的項目
選擇添加標識,然后會從基架檢索信息:
再次出現選擇 標識 可以重載的文件:
這次先把 用戶自己維護信息的 Manage\Index加一下,加完以后,Areas->Identity->Pages->Account 多了一個Manage 目錄: 下面就有Index.cshtml
默認 /Identity/Account/Manage/ 頁面為: (可以修改手機號碼、email、密碼、雙因素認證、以及個人賬號信息的下載或刪除)
其中 雙因素認證,本次先不考慮,則准備先去除:
直接打開 \Areas\Identity\Pages\Account\Manage\_ManageNav.cshtml,然后注釋掉一些代碼:
@inject SignInManager<IdentityUser> SignInManager @{ var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); } <ul class="nav nav-pills flex-column"> <li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li> <li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li> <li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li> @*@if (hasExternalLogins) { <li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li> } <li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>*@ <li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li> </ul>
修改后:
后續准備找份詳細資料,能夠羅列出 Identity 下所有各文件選項對應功能意義,這樣就可以根據自己實際情況來考慮是不是要重載。