ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立
前言:
本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。
本系列文章主要参考资料:
微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows
《Pro ASP.NET MVC 5》、《锋利的 jQuery》
当此系列文章写完后会在一周内推出修正版。
此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。
项目 github 地址:https://github.com/NanaseRuri/LibraryDemo
本章内容:Identity 框架的配置、对账户进行授权的配置、数据库的初始化方法、自定义 TagHelper
一到四为对 Student 即 Identity框架的使用,第五节为对 Admin 用户的配置
一、自定义账号和密码的限制
在 Startup.cs 的 ConfigureServices 方法中可以对 Identity 的账号和密码进行限制:
services.AddIdentity<Student, IdentityRole>(opts => { opts.User.RequireUniqueEmail = true; opts.User.AllowedUserNameCharacters = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789"; opts.Password.RequiredLength = 6; opts.Password.RequireNonAlphanumeric = false; opts.Password.RequireLowercase = false; opts.Password.RequireUppercase = false; opts.Password.RequireDigit = false; }).AddEntityFrameworkStores<StudentIdentityDbContext>() .AddDefaultTokenProviders();
RequireUniqueEmail 限制每个邮箱只能用于一个账号。
此处 AllowedUserNameCharacters 方法限制用户名能够使用的字符,需要单独输入每个字符。
剩下的设置分别为限制密码必须有符号 / 包含小写字母 / 包含大写字母 / 包含数字。
二、对数据库进行初始化
在此创建一个 StudentInitiator 用以对数据库进行初始化:
public class StudentInitiator { public static async Task Initial(IServiceProvider serviceProvider) { UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>(); if (userManager.Users.Any()) { return; } IEnumerable<Student> initialStudents = new[] { new Student() { UserName = "U201600001", Name = "Nanase", Email = "Nanase@cnblog.com", PhoneNumber = "12345678910", Degree = Degrees.CollegeStudent, MaxBooksNumber = 10, }, new Student() { UserName = "U201600002", Name = "Ruri", Email = "NanaseRuri@cnblog.com", PhoneNumber = "12345678911", Degree = Degrees.DoctorateDegree, MaxBooksNumber = 15 }, }; foreach (var student in initialStudents) { await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6,6)); } } }
为确保能够进行初始化,在 Startup.cs 的 Configure 方法中调用该静态方法:
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); DatabaseInitiator.Initial(app.ApplicationServices).Wait();
Initial 方法中 serviceProvider 参数将在传入 ConfigureServices 方法调用后的 ServiceProvider,此时在 Initial 方法中初始化的数据也会使用 ConfigureServices 中对账号和密码的限制。
此处我们使用账号的后六位作为密码。启动网页后查看数据库的数据:
三、建立验证所用的控制器以及视图
首先创建一个视图模型用于存储账号的信息,为了方便实现多种登录方式,此处创建一个 LoginType 枚举:
[UIHint] 特性构造函数传入一个字符串用来告知对应属性在使用 Html.EditorFor() 时用什么模板来展示数据。
public enum LoginType { UserName, Email, Phone } public class LoginModel { [Required(ErrorMessage = "请输入您的学号 / 邮箱 / 手机号码")] [Display(Name = "学号 / 邮箱 / 手机号码")] public string Account { get; set; } [Required(ErrorMessage = "请输入您的密码")] [UIHint("password")] [Display(Name = "密码")] public string Password { get; set; } [Required] public LoginType LoginType { get; set; } }
使用支架特性创建一个 StudentAccountController
StudentAccount 控制器:
第 5 行判断是否授权以避免多余的授权:
public class StudentAccountController : Controller { public IActionResult Login(string returnUrl) { if (HttpContext.User.Identity.IsAuthenticated) { return RedirectToAction("AccountInfo"); } LoginModel loginInfo = new LoginModel(); ViewBag.returnUrl = returnUrl; return View(loginInfo); } }
在在 Login 视图中添加多种登录方式,并使视图更加清晰,创建了一个 LoginTypeTagHelper ,TagHelper 可制定自定义 HTML 标记并在最终生成视图时转换成标准的 HTML 标记。
[HtmlTargetElement("LoginType")] public class LoginTypeTagHelper:TagHelper { public string[] LoginType { get; set; } public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { foreach (var loginType in LoginType) { switch (loginType) { case "UserName": output.Content.AppendHtml($"<option selected=\"selected/\" value=\"{loginType}\">学号</option>"); break; case "Email": output.Content.AppendHtml(GetOption(loginType, "邮箱")); break; case "Phone": output.Content.AppendHtml(GetOption(loginType, "手机号码")); break; default: break; } } return Task.CompletedTask; } private static string GetOption(string loginType,string innerText) { return $"<option value=\"{loginType}\">{innerText}</option>"; } }
Login 视图:
25 行中使用了刚建立的 LoginTypeTagHelper:
@model LoginModel @{ ViewData["Title"] = "Login"; } <h2>Login</h2> <br/> <div class="text-danger" asp-validation-summary="All"></div> <br/> <form asp-action="Login" method="post"> <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl"/> <div class="form-group"> <label asp-for="Account"></label> <input asp-for="Account" class="form-control" placeholder="请输入你的学号 / 邮箱 / 手机号"/> </div> <div class="form-group"> <label asp-for="Password"></label> <input asp-for="Password" class="form-control" placeholder="请输入你的密码"/> </div> <div class="form-group"> <label>登录方式</label> <select asp-for="LoginType"> <option disabled value="">登录方式</option> <LoginType login-type="@Enum.GetNames(typeof(LoginType))"></LoginType> </select> </div> <input type="submit" class="btn btn-primary"/> </form>
然后创建一个用于对信息进行验证的动作方法。
为了获取数据库的数据以及对数据进行验证授权,需要通过 DI(依赖注入) 获取对应的 UserManager 和 SignInManager 对象,在此针对 StudentAccountController 的构造函数进行更新。
StudentAccountController 整体:
[Authorize] public class StudentAccountController : Controller { private UserManager<Student> _userManager; private SignInManager<Student> _signInManager; public StudentAccountController(UserManager<Student> studentManager, SignInManager<Student> signInManager) { _userManager = studentManager; _signInManager = signInManager; } [AllowAnonymous] public IActionResult Login(string returnUrl) { if (HttpContext.User.Identity.IsAuthenticated) { return RedirectToAction("AccountInfo"); } LoginModel loginInfo = new LoginModel(); ViewBag.returnUrl = returnUrl; return View(loginInfo); } [HttpPost] [ValidateAntiForgeryToken] [AllowAnonymous] public async Task<IActionResult> Login(LoginModel loginInfo, string returnUrl) { if (ModelState.IsValid) { Student student =await GetStudentByLoginModel(loginInfo); if (student == null) { return View(loginInfo); } SignInResult signInResult = await _signInManager.PasswordSignInAsync(student, loginInfo.Password, false, false); if (signInResult.Succeeded) { return Redirect(returnUrl ?? "/StudentAccount/"+nameof(AccountInfo)); } ModelState.AddModelError("", "账号或密码错误"); } return View(loginInfo); } public IActionResult AccountInfo() { return View(CurrentAccountData()); } Dictionary<string, object> CurrentAccountData() { var userName = HttpContext.User.Identity.Name; var user = _userManager.FindByNameAsync(userName).Result; return new Dictionary<string, object>() { ["学号"]=userName, ["姓名"]=user.Name, ["邮箱"]=user.Email, ["手机号"]=user.PhoneNumber, }; } }
_userManager 以及 _signInManager 将通过 DI 获得实例;[ValidateAntiForgeryToken] 特性用于防止 XSRF 攻击;returnUrl 参数用于接收或返回之前正在访问的页面,在此处若 returnUrl 为空则返回 AccountInfo 页面;[Authorize] 特性用于确保只有已授权的用户才能访问对应动作方法;CurrentAccountData 方法用于获取当前用户的信息以在 AccountInfo 视图中呈现。
由于未进行授权,在此直接访问 AccountInfo 方法默认会返回 /Account/Login 页面请求验证,可通过在 Startup.cs 的 ConfigureServices 方法进行配置以覆盖这一行为,让页面默认返回 /StudentAccount/Login :
services.ConfigureApplicationCookie(opts => { opts.LoginPath = "/StudentAccount/Login"; }
为了使 [Authorize] 特性能够正常工作,需要在 Configure 方法中使用 Authentication 中间件,如果没有调用 app.UseAuthentication(),则访问带有 [Authorize] 的方法会再度要求进行验证。中间件的顺序很重要:
app.UseAuthentication(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy();
直接访问 AccountInfo 页面:
输入账号密码进行验证:
验证之后返回 /StudentAccount/AccountInfo 页面:
四、创建登出网页
简单地调用 SignOutAsync 用以清除当前 Cookie 中的授权信息。
public async Task<IActionResult> Logout(string returnUrl) { await _signInManager.SignOutAsync(); if (returnUrl == null) { return View("Login"); } return Redirect(returnUrl); }
同时在 AccountInfo 添加登出按钮:
@model Dictionary<string, object> @{ ViewData["Title"] = "AccountInfo"; } <h2>账户信息</h2> <ul> @foreach (var info in Model) { <li>@info.Key: @Model[info.Key]</li> } </ul> <br /> <a class="btn btn-danger" asp-action="Logout">登出</a>
登出后返回 Login 页面,同时 AccountInfo 页面需要重新进行验证。
附加使用邮箱以及手机号验证的测试:
五、基于 Role 的 Identity 授权
修改 StudentInitial 类,添加名为 admin 的学生数组并使用 AddToRoleAsync 为用户添加身份。在添加 Role 之前需要在 RoleManager 对象中使用 Create 方法为 Role 数据库添加特定的 Role 字段:
public class StudentInitiator { public static async Task InitialStudents(IServiceProvider serviceProvider) { UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>(); RoleManager<IdentityRole> roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>(); if (userManager.Users.Any()) { return; } if (await roleManager.FindByNameAsync("Admin")==null) { await roleManager.CreateAsync(new IdentityRole("Admin")); } if (await roleManager.FindByNameAsync("Student")==null) { await roleManager.CreateAsync(new IdentityRole("Student")); } IEnumerable<Student> initialStudents = new[] { new Student() { UserName = "U201600001", Name = "Nanase", Email = "Nanase@cnblog.com", PhoneNumber = "12345678910", Degree = Degrees.CollegeStudent, MaxBooksNumber = 10, }, new Student() { UserName = "U201600002", Name = "Ruri", Email = "NanaseRuri@cnblog.com", PhoneNumber = "12345678911", Degree = Degrees.DoctorateDegree, MaxBooksNumber = 15 } }; IEnumerable<Student> initialAdmins = new[] { new Student() { UserName = "A000000000", Name="Admin0000", Email = "Admin@cnblog.com", PhoneNumber = "12345678912", Degree = Degrees.CollegeStudent, MaxBooksNumber = 20 }, new Student() { UserName = "A000000001", Name = "Admin0001", Email = "123456789@qq.com", PhoneNumber = "12345678910", Degree = Degrees.CollegeStudent, MaxBooksNumber = 20 }, }; foreach (var student in initialStudents) { await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6, 6)); } foreach (var admin in initialAdmins) { await userManager.CreateAsync(admin, "zxcZXC!123"); await userManager.AddToRoleAsync(admin, "Admin"); } } }
对 ConfigureServices 作进一步配置,添加 Cookie 的过期时间和不满足 Authorize 条件时返回的 Url:
1 services.ConfigureApplicationCookie(opts => 2 { 3 opts.Cookie.HttpOnly = true; 4 opts.LoginPath = "/StudentAccount/Login"; 5 opts.AccessDeniedPath = "/StudentAccount/Login"; 6 opts.ExpireTimeSpan=TimeSpan.FromMinutes(5); 7 });
则当 Role 不为 Admin 时将返回 /StudentAccount/Login 而非默认的 /Account/AccessDeny。
然后新建一个用以管理学生信息的 AdminAccount 控制器,设置 [Authorize] 特性并指定 Role 属性,使带有特定 Role 的身份才可以访问该控制器。
[Authorize(Roles = "Admin")] public class AdminAccountController : Controller { private UserManager<Student> _userManager; public AdminAccountController(UserManager<Student> userManager) { _userManager = userManager; } public IActionResult Index() { ICollection<Student> students = _userManager.Users.ToList(); return View(students); } }
Index 视图:
@using LibraryDemo.Models.DomainModels @model IEnumerable<LibraryDemo.Models.DomainModels.Student> @{ ViewData["Title"] = "AccountInfo"; Student stu = new Student(); } <link rel="stylesheet" href="~/css/BookInfo.css" /> <script> function confirmDelete() { var userNames = document.getElementsByName("userNames"); var message = "确认删除"; var values = []; for (i in userNames) { if (userNames[i].checked) { message = message + userNames[i].value+","; values.push(userNames[i].value); } } message = message + "?"; if (confirm(message)) { $.ajax({ url: "@Url.Action("RemoveStudent")", contentType: "application/json", method: "POST", data: JSON.stringify(values), success: function(students) { updateTable(students); } }); } } function updateTable(data) { var body = $("#studentList"); body.empty(); for (var i = 0; i < data.length; i++) { var person = data[i]; body.append(`<tr><td><input type="checkbox" name="userNames" value="${person.userName}" /></td> <td>${person.userName}</td><td>${person.name}</td><td>${person.degree}</td> <td>${person.phoneNumber}</td><td>${person.email}</td><td>${person.maxBooksNumber}</td></tr>`); } }; function addStudent() { var studentList = $("#studentList"); if (!document.getElementById("studentInfo")) { studentList.append('<tr id="studentInfo">' + '<td></td>' + '<td><input type="text" name="UserName" id="UserName" /></td>' + '<td><input type="text" name="Name" id="Name" /></td>' + '<td><input type="text" name="Degree" id="Degree" /></td>' + '<td><input type="text" name="PhoneNumber" id="PhoneNumber" /></td>' + '<td><input type="text" name="Email" id="Email" /></td>' + '<td><input type="text" name="MaxBooksNumber" id="MaxBooksNumber" /></td>' + '<td><button type="submit" onclick="return postAddStudent()">添加</button></td>' + '</tr>'); } } function postAddStudent() { $.ajax({ url: "@Url.Action("AddStudent")", contentType: "application/json", method: "POST", data: JSON.stringify({ UserName: $("#UserName").val(), Name: $("#Name").val(), Degree:$("#Degree").val(), PhoneNumber: $("#PhoneNumber").val(), Email: $("#Email").val(), MaxBooksNumber: $("#MaxBooksNumber").val() }), success: function (student) { addStudentToTable(student); } }); } function addStudentToTable(student) { var studentList = document.getElementById("studentList"); var studentInfo = document.getElementById("studentInfo"); studentList.removeChild(studentInfo); $("#studentList").append(`<tr>` + `<td><input type="checkbox" name="userNames" value="${student.userName}" /></td>` + `<td>${student.userName}</td>` + `<td>${student.name}</td>`+ `<td>${student.degree}</td>` + `<td>${student.phoneNumber}</td>` + `<td>${student.email}</td>` + `<td>${student.maxBooksNumber}</td >` + `</tr>`); } </script> <h2>学生信息</h2> <div id="buttonGroup"> <button class="btn btn-primary" onclick="return addStudent()">添加学生</button> <button class="btn btn-danger" onclick="return confirmDelete()">删除学生</button> </div> <br /> <table> <thead> <tr> <th></th> <th>@Html.LabelFor(m => stu.UserName)</th> <th>@Html.LabelFor(m => stu.Name)</th> <th>@Html.LabelFor(m => stu.Degree)</th> <th>@Html.LabelFor(m => stu.PhoneNumber)</th> <th>@Html.LabelFor(m => stu.Email)</th> <th>@Html.LabelFor(m => stu.MaxBooksNumber)</th> </tr> </thead> <tbody id="studentList"> @if (!@Model.Any()) { <tr><td colspan="6">未有学生信息</td></tr> } else { foreach (var student in Model) { <tr> <td><input type="checkbox" name="userNames" value="@student.UserName" /></td> <td>@student.UserName</td> <td>@student.Name</td> <td>@Html.DisplayFor(m => student.Degree)</td> <td>@student.PhoneNumber</td> <td>@student.Email</td> <td>@student.MaxBooksNumber</td> </tr> } } </tbody> </table>
使用 Role 不是 Admin 的账户登录:
使用 Role 为 Admin 的账户登录:
- ASP.NET Core 打造一个简单的图书馆管理系统(三)基本登录页面以及授权逻辑的建立
- ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(一) 基本模型以及数据库的建立
- ASP.NET Core 打造一个简单的图书馆管理系统(五)初始化书籍信息
- ASP.NET Core 打造一个简单的图书馆管理系统(二)Code First 多对多关系的建立
- ASP.NET Core 打造一个简单的图书馆管理系统(四)密码修改以及密码重置
- ASP.NET Core 打造一个简单的图书馆管理系统(九) 学生信息增删(终章)
- ASP.NET Core 打造一个简单的图书馆管理系统(七)外借/阅览图书信息的增删改查
- Asp.Net Core 项目实战之权限管理系统(5) 用户登录
- 用ASP.NET实现简单的超市管理系统-登录页面
- Asp .Net Core 2.0 登录授权以及多用户登录
- 一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之完成登录功能
- 一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之登录窗口
- 一步步打造基于ASP.NET的CMS内容管理系统--Step3 添加新闻页面
- asp.net 单用户登录(系统中只允许同一账户的一个存在--排它多处登录)
- sql server 关于表中只增标识问题 C# 实现自动化打开和关闭可执行文件(或 关闭停止与系统交互的可执行文件) ajaxfileupload插件上传图片功能,用MVC和aspx做后台各写了一个案例 将小写阿拉伯数字转换成大写的汉字, C# WinForm 中英文实现, 国际化实现的简单方法 ASP.NET Core 2 学习笔记(六)ASP.NET Core 2 学习笔记(三)
- 一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之登录窗口
- 一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之登录窗口调试
- ASP.NET jQuery 食谱11 (通过使用jQuery validation插件简单实现用户登录页面验证功能)
- 一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之登录窗口
- 用asp.net开发的一个系统,如何给用户提供数据库备份和恢复的功能?