手头一个小应用项目,非常非常小。。。
主要功能也就是提供一个网页页面浏览数据,数据内容部分在数据库中,图像部分在阿里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 下所有各文件选项对应功能意义,这样就可以根据自己实际情况来考虑是不是要重载。