ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(二)数据库初始化、基本登录页面以及授权逻辑的建立
前言:
本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。
本系列文章主要参考资料:
微软文档: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 的账号和密码进行限制:
1 services.AddIdentity<Student, IdentityRole>(opts => 2 { 3 4 opts.User.RequireUniqueEmail = true; 5 opts.User.AllowedUserNameCharacters = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789"; 6 opts.Password.RequiredLength = 6; 7 opts.Password.RequireNonAlphanumeric = false; 8 opts.Password.RequireLowercase = false; 9 opts.Password.RequireUppercase = false; 10 opts.Password.RequireDigit = false; 11 }).AddEntityFrameworkStores<StudentIdentityDbContext>() 12 .AddDefaultTokenProviders();
RequireUniqueEmail 限制每个邮箱只能用于一个账号。
此处 AllowedUserNameCharacters 方法限制用户名能够使用的字符,需要单独输入每个字符。
剩下的设置分别为限制密码必须有符号 / 包含小写字母 / 包含大写字母 / 包含数字。
二、对数据库进行初始化
在此创建一个 StudentInitiator 以及一个 BookInitiator用以对数据库进行初始化:
1 public class StudentInitiator 2 { 3 public static async Task Initial(IServiceProvider serviceProvider) 4 { 5 UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>(); 6 if (userManager.Users.Any()) 7 { 8 return; 9 } 10 IEnumerable<Student> initialStudents = new[] 11 { 12 new Student() 13 { 14 UserName = "U201600001", 15 Name = "Nanase", 16 Email = "Nanase@cnblog.com", 17 PhoneNumber = "12345678910", 18 Degree = Degrees.CollegeStudent, 19 MaxBooksNumber = 10, 20 }, 21 new Student() 22 { 23 UserName = "U201600002", 24 Name = "Ruri", 25 Email = "NanaseRuri@cnblog.com", 26 PhoneNumber = "12345678911", 27 Degree = Degrees.DoctorateDegree, 28 MaxBooksNumber = 15 29 }, 30 }; 31 32 foreach (var student in initialStudents) 33 { 34 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6,6)); 35 } 36 } 37 }
新建 BookInitiator 用于初始化该数据库,不知道为什么在这里会报一个 Book 重复主键的错误,但是数据确实全都插进去了:
1 public class BookInitiator 2 { 3 public static async Task BookInitial(IServiceProvider serviceProvider) 4 { 5 LendingInfoDbContext context = serviceProvider.GetRequiredService<LendingInfoDbContext>(); 6 if (!context.Books.Any() || !context.Bookshelves.Any()) 7 { 8 Bookshelf[] bookshelfs = new[] 9 { 10 new Bookshelf() 11 { 12 BookshelfId = 1, 13 Location = "主校区", 14 Sort = "计算机" 15 }, 16 new Bookshelf() 17 { 18 BookshelfId = 2, 19 Location = "主校区", 20 Sort = "文学" 21 }, 22 new Bookshelf() 23 { 24 BookshelfId = 3, 25 Location = "东校区", 26 Sort = "计算机" 27 }, 28 new Bookshelf() 29 { 30 BookshelfId = 4, 31 Location = "阅览室", 32 Sort = "文学" 33 }, 34 new Bookshelf() 35 { 36 BookshelfId = 5, 37 Location = "阅览室", 38 Sort = "计算机" 39 }, 40 }; 41 42 Book[] books = new[] 43 { 44 new Book() 45 { 46 Name = "精通ASP.NET MVC 5", 47 BarCode = "001100987211", 48 ISBN = "978-7-115-41023-8", 49 State = BookState.Normal, 50 FetchBookNumber = "TP393.092 19", 51 Location = "主校区", 52 Sort = "计算机" 53 }, 54 new Book() 55 { 56 Name = "精通ASP.NET MVC 5", 57 BarCode = "001100987212", 58 ISBN = "978-7-115-41023-8", 59 State = BookState.Normal, 60 FetchBookNumber = "TP393.092 19", 61 Location = "主校区", 62 Sort = "计算机" 63 }, 64 new Book() 65 { 66 Name = "精通ASP.NET MVC 5", 67 BarCode = "001100987213", 68 ISBN = "978-7-115-41023-8", 69 State = BookState.Normal, 70 FetchBookNumber = "TP393.092 19", 71 Location = "东校区", 72 Sort = "计算机" 73 }, 74 new Book() 75 { 76 Name = "精通ASP.NET MVC 5", 77 BarCode = "001100987214", 78 ISBN = "978-7-115-41023-8", 79 State = BookState.Readonly, 80 FetchBookNumber = "TP393.092 19", 81 Location = "阅览室", 82 Sort = "计算机" 83 }, 84 new Book() 85 { 86 Name = "Entity Framework实用精要", 87 BarCode = "001101279682", 88 ISBN = "978-7-302-48593-3", 89 State = BookState.Normal, 90 FetchBookNumber = "TP393.09 447", 91 Location = "主校区", 92 Sort = "计算机" 93 }, 94 new Book() 95 { 96 Name = "Entity Framework实用精要", 97 BarCode = "001101279683", 98 ISBN = "978-7-302-48593-3", 99 State = BookState.Normal, 100 FetchBookNumber = "TP393.09 447", 101 Location = "主校区", 102 Sort = "计算机" 103 }, 104 new Book() 105 { 106 Name = "Entity Framework实用精要", 107 BarCode = "001101279684", 108 ISBN = "978-7-302-48593-3", 109 State = BookState.Normal, 110 FetchBookNumber = "TP393.09 447", 111 Location = "东校区", 112 Sort = "计算机" 113 }, 114 new Book() 115 { 116 Name = "Entity Framework实用精要", 117 BarCode = "001101279685", 118 ISBN = "978-7-302-48593-3", 119 State = BookState.Normal, 120 FetchBookNumber = "TP393.09 447", 121 Location = "东校区", 122 Sort = "计算机" 123 }, 124 new Book() 125 { 126 Name = "Entity Framework实用精要", 127 BarCode = "001101279686", 128 ISBN = "978-7-302-48593-3", 129 State = BookState.Normal, 130 FetchBookNumber = "TP393.09 447", 131 Location = "阅览室", 132 Sort = "计算机" 133 }, 134 new Book() 135 { 136 Name = "Rails 5敏捷开发", 137 BarCode = "001101290497", 138 ISBN = "978-7-5680-3659-7", 139 State = BookState.Normal, 140 FetchBookNumber = "TP393.09 448", 141 Location = "主校区", 142 Sort = "计算机" 143 }, 144 new Book() 145 { 146 Name = "Rails 5敏捷开发", 147 BarCode = "001101290498", 148 ISBN = "978-7-5680-3659-7", 149 State = BookState.Normal, 150 FetchBookNumber = "TP393.09 448", 151 Location = "主校区", 152 Sort = "计算机" 153 }, 154 new Book() 155 { 156 Name = "Rails 5敏捷开发", 157 BarCode = "001101290499", 158 ISBN = "978-7-5680-3659-7", 159 State = BookState.Readonly, 160 FetchBookNumber = "TP393.09 448", 161 Location = "主校区", 162 Sort = "计算机" 163 }, 164 new Book() 165 { 166 Name = "你必须掌握的Entity Framework 6.x与Core 2.0", 167 BarCode = "001101362986", 168 ISBN = "978-7-5680-3659-7", 169 State = BookState.Normal, 170 FetchBookNumber = "TP393.09 452", 171 Location = "主校区", 172 Sort = "计算机" 173 }, 174 new Book() 175 { 176 Name = "你必须掌握的Entity Framework 6.x与Core 2.0", 177 BarCode = "001101362987", 178 ISBN = "978-7-5680-3659-7", 179 State = BookState.Readonly, 180 FetchBookNumber = "TP393.09 452", 181 Location = "主校区", 182 Sort = "计算机" 183 }, 184 new Book() 185 { 186 Name = "毛选. 第一卷", 187 BarCode = "00929264", 188 ISBN = "7-01-000922-8", 189 State = BookState.Normal, 190 FetchBookNumber = "A41 1:1", 191 Location = "主校区", 192 Sort = "文学" 193 }, 194 new Book() 195 { 196 Name = "毛选. 第一卷", 197 BarCode = "00929265", 198 ISBN = "7-01-000922-8", 199 State = BookState.Normal, 200 FetchBookNumber = "A41 1:1", 201 Location = "主校区", 202 Sort = "文学" 203 }, 204 new Book() 205 { 206 Name = "毛选. 第一卷", 207 BarCode = "00929266", 208 ISBN = "7-01-000922-8", 209 State = BookState.Readonly, 210 FetchBookNumber = "A41 1:1", 211 Location = "阅览室", 212 Sort = "文学" 213 }, 214 new Book() 215 { 216 Name = "毛选. 第二卷", 217 BarCode = "00929279", 218 ISBN = "7-01-000915-5", 219 State = BookState.Normal, 220 FetchBookNumber = "A41 1:2", 221 Location = "主校区", 222 Sort = "文学" 223 }, 224 new Book() 225 { 226 Name = "毛选. 第二卷", 227 BarCode = "00929280", 228 ISBN = "7-01-000915-5", 229 State = BookState.Readonly, 230 FetchBookNumber = "A41 1:2", 231 Location = "阅览室", 232 Sort = "文学" 233 }, 234 new Book() 235 { 236 Name = "毛选. 第三卷", 237 BarCode = "00930420", 238 ISBN = "7-01-000916-3", 239 State = BookState.Normal, 240 FetchBookNumber = "A41 1:3", 241 Location = "主校区", 242 Sort = "文学" 243 }, 244 new Book() 245 { 246 Name = "毛选. 第三卷", 247 BarCode = "00930421", 248 ISBN = "7-01-000916-3", 249 State = BookState.Readonly, 250 FetchBookNumber = "A41 1:3", 251 Location = "阅览室", 252 Sort = "文学" 253 }, 254 new Book() 255 { 256 Name = "毛选. 第四卷", 257 BarCode = "00930465", 258 ISBN = "7-01-000925-2", 259 State = BookState.Normal, 260 FetchBookNumber = "A41 1:4", 261 Location = "主校区", 262 Sort = "文学" 263 }, 264 new Book() 265 { 266 Name = "毛选. 第四卷", 267 BarCode = "00930466", 268 ISBN = "7-01-000925-2", 269 State = BookState.Readonly, 270 FetchBookNumber = "A41 1:4", 271 Location = "阅览室", 272 Sort = "文学" 273 } 274 }; 275 276 BookDetails[] bookDetails = new[] 277 { 278 new BookDetails() 279 { 280 Author = "Admam Freeman", 281 Name = "精通ASP.NET MVC 5", 282 ISBN = "978-7-115-41023-8", 283 Press = "人民邮电出版社", 284 PublishDateTime = new DateTime(2016,1,1), 285 SoundCassettes = "13, 642页 : 图 ; 24cm", 286 Version = 1, 287 FetchBookNumber = "TP393.092 19", 288 Description = "ASP.NET MVC 5框架是微软ASP.NET Web平台的新进展。它提供了高生产率的编程模型,结合ASP.NET的全部优势,促成更整洁的代码架构、测试驱动开发和强大的可扩展性。本书涵盖ASP.NET MVC 5的所有开发优势技术,包括用C#属性定义路由技术及重写过滤器技术等。且构建MVC应用程序的用户体验也有本质上的改进。其中书里也专一讲解了用新Visual Studio 2013创建MVC应用程序时的技术和技巧。本书包括完整的开发工具介绍以及对代码进行辅助编译和调试的技术。本书还涉及流行的Bootstrap JavaScript库,该库现已被纳入到MVC 5之中,为开发人员提供更广泛的多平台CSS和HTML5选项,而不必像以前那样去加载大量的第三方库。" 289 }, 290 new BookDetails() 291 { 292 Author = "吕高旭", 293 Name = "Entity Framework实用精要", 294 ISBN = "978-7-302-48593-3", 295 Press = "清华大学出版社", 296 PublishDateTime = new DateTime(2018,1,1), 297 SoundCassettes = "346页 ; 26cm", 298 Version = 1, 299 FetchBookNumber = "TP393.09 447", 300 Description = "本书通过介绍Entity Framework与 LINQ 开发实战的案例,以 Entity Framework 技术内容的讨论为主线,结合关键的 LINQ技巧说明,提供读者系统性学习 Entity Framework 所需的内容。本书旨在帮助读者进入 Entity Framework的世界,建立必要的技术能力,同时希望读者在完成本书的教学课程之后,能够更进一步地将其运用在实际的项目开发中。" 301 }, 302 new BookDetails() 303 { 304 Author = "鲁比", 305 Name = "Rails 5敏捷开发", 306 ISBN = "978-7-5680-3659-7", 307 Press = "华中科技大学出版社", 308 PublishDateTime = new DateTime(2018,1,1), 309 SoundCassettes = "xxi, 451页 : 图 ; 23cm", 310 Version = 1, 311 FetchBookNumber = "TP393.09 448", 312 Description = "本书以讲解“购书网站”案例为主线, 逐步介绍Rails的内置功能。全书分为3部分, 第一部分介绍Rails的安装、应用程序验证、Rails框架的体系结构, 以及Ruby语言知识; 第二部分用迭代方式构建应用程序, 然后依据敏捷开发模式开展测试, 最后用Capistrano完成部署; 第三部分补充日常实用的开发知识。本书既有直观的示例, 又有深入的分析, 同时涵盖了Web开发各方面的知识, 堪称一部内容全面而又深入浅出的佳作。第5版增加了关于Rails 5和Ruby 2.2新特性和最佳实践的内容。" 313 }, 314 new BookDetails() 315 { 316 Author = "汪鹏", 317 Name = "你必须掌握的Entity Framework 6.x与Core 2.0", 318 ISBN = "978-7-302-50017-9", 319 Press = "清华大学出版社", 320 PublishDateTime = new DateTime(2018,1,1), 321 SoundCassettes = "X, 487页 : 图 ; 26cm", 322 Version = 1, 323 FetchBookNumber = "TP393.09 452", 324 Description = "本书分为四篇,第一篇讲解Entity Framework 6.x的基础,包括数据库表的创建,数据的操作和数据加载方式。第二篇讲解Entity Framework 6.x进阶,包括基本原理和性能优化。第三篇讲解跨平台Entity Framework Core 2.x的基础知识和开发技巧。第四篇讲解在Entity Framework Core 2.x中解决并发问题,并给出实战开发案例。" 325 }, 326 new BookDetails() 327 { 328 Author = "毛", 329 Name = "毛选. 第一卷", 330 ISBN = "7-01-000914-7", 331 Press = "人民出版社", 332 PublishDateTime = new DateTime(1991,1,1), 333 SoundCassettes = "340页 : 肖像 ; 19厘米", 334 FetchBookNumber = "A41 1:1", 335 Version = 2, 336 Description = "《毛选》是对20世纪中国影响最大的书籍之一。" 337 }, 338 new BookDetails() 339 { 340 Author = "毛", 341 Name = "毛选. 第二卷", 342 ISBN = "7-01-000915-5", 343 Press = "人民出版社", 344 PublishDateTime = new DateTime(1991,1,1), 345 SoundCassettes = "343-786页 : 肖像 ; 19厘米", 346 Version = 2, 347 FetchBookNumber = "A41 1:2", 348 Description = "《毛选》是对20世纪中国影响最大的书籍之一。" 349 }, 350 new BookDetails() 351 { 352 Author = "毛", 353 Name = "毛选. 第三卷", 354 ISBN = "7-01-000916-3", 355 Press = "人民出版社", 356 PublishDateTime = new DateTime(1991,1,1), 357 SoundCassettes = "789Ł±1120页 ; 20厘米", 358 FetchBookNumber = "A41 1:3", 359 Version = 2, 360 Description = "《毛选》是对20世纪中国影响最大的书籍之一。" 361 }, 362 new BookDetails() 363 { 364 Author = "毛", 365 Name = "毛选. 第四卷", 366 ISBN = "7-01-000925-2", 367 Press = "人民出版社", 368 PublishDateTime = new DateTime(1991,1,1), 369 SoundCassettes = "1123Ł±1517页 ; 20厘米", 370 FetchBookNumber = "A41 1:4", 371 Version = 2, 372 Description = "《毛选》是对20世纪中国影响最大的书籍之一。" 373 }, 374 }; 375 376 var temp = from book in books 377 from bookshelf in bookshelfs 378 where book.Location == bookshelf.Location && book.Sort == bookshelf.Sort 379 select new { BarCode = book.BarCode, BookshelfId = bookshelf.BookshelfId }; 380 381 foreach (var bookshelf in bookshelfs) 382 { 383 bookshelf.Books=new List<Book>(); 384 } 385 386 foreach (var tem in temp) 387 { 388 Bookshelf targetShelf = bookshelfs.Single(bookshelf => bookshelf.BookshelfId == tem.BookshelfId); 389 Book targetBook = books.Single(book => book.BarCode == tem.BarCode); 390 targetShelf.Books.Add(targetBook); 391 } 392 393 foreach (var bookshelf in bookshelfs) 394 { 395 bookshelf.MaxFetchNumber=bookshelf.Books.Max(b => b.FetchBookNumber); 396 bookshelf.MinFetchNumber=bookshelf.Books.Min(b => b.FetchBookNumber); 397 } 398 399 foreach (var bookshelf in bookshelfs) 400 { 401 await context.Bookshelves.AddAsync(bookshelf); 402 await context.SaveChangesAsync(); 403 } 404 405 foreach (var bookDetail in bookDetails) 406 { 407 await context.BooksDetail.AddAsync(bookDetail); 408 await context.SaveChangesAsync(); 409 } 410 411 foreach (var book in books) 412 { 413 await context.Books.AddAsync(book); 414 await context.SaveChangesAsync(); 415 } 416 } 417 } 418 }
为确保能够进行初始化,在 Startup.cs 的 Configure 方法中调用该静态方法:
1 DatabaseInitiator.Initial(app.ApplicationServices).Wait(); 2 BookInitiator.BookInitial(app.ApplicationServices).Wait();
Initial 方法中 serviceProvider 参数将在传入 ConfigureServices 方法调用后的 ServiceProvider,此时在 Initial 方法中初始化的数据也会使用 ConfigureServices 中对账号和密码的限制。
此处我们使用账号的后六位作为密码。启动网页后查看数据库的数据:
三、建立验证所用的控制器以及视图
首先创建一个视图模型用于存储账号的信息,为了方便实现多种登录方式,此处创建一个 LoginType 枚举:
[UIHint] 特性构造函数传入一个字符串用来告知对应属性在使用 Html.EditorFor() 时用什么模板来展示数据。
1 public enum LoginType 2 { 3 UserName, 4 Email, 5 Phone 6 } 7 8 public class LoginModel 9 { 10 [Required(ErrorMessage = "请输入您的学号 / 邮箱 / 手机号码")] 11 [Display(Name = "学号 / 邮箱 / 手机号码")] 12 public string Account { get; set; } 13 14 [Required(ErrorMessage = "请输入您的密码")] 15 [UIHint("password")] 16 [Display(Name = "密码")] 17 public string Password { get; set; } 18 19 [Required] 20 public LoginType LoginType { get; set; } 21 }
使用支架特性创建一个 StudentAccountController
StudentAccount 控制器:
第 5 行判断是否授权以避免多余的授权:
1 public class StudentAccountController : Controller 2 { 3 public IActionResult Login(string returnUrl) 4 { 5 if (HttpContext.User.Identity.IsAuthenticated) 6 { 7 return RedirectToAction("AccountInfo"); 8 } 9 10 LoginModel loginInfo = new LoginModel(); 11 ViewBag.returnUrl = returnUrl; 12 return View(loginInfo); 13 } 14 }
在在 Login 视图中添加多种登录方式,并使视图更加清晰,创建了一个 LoginTypeTagHelper ,TagHelper 可制定自定义 HTML 标记并在最终生成视图时转换成标准的 HTML 标记。
1 [HtmlTargetElement("LoginType")] 2 public class LoginTypeTagHelper:TagHelper 3 { 4 public string[] LoginType { get; set; } 5 6 public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 7 { 8 foreach (var loginType in LoginType) 9 { 10 switch (loginType) 11 { 12 case "UserName": output.Content.AppendHtml($"<option selected=\"selected/\" value=\"{loginType}\">学号</option>"); 13 break; 14 case "Email": output.Content.AppendHtml(GetOption(loginType, "邮箱")); 15 break; 16 case "Phone": output.Content.AppendHtml(GetOption(loginType, "手机号码")); 17 break; 18 default: break; 19 } 20 } 21 return Task.CompletedTask; 22 } 23 24 private static string GetOption(string loginType,string innerText) 25 { 26 return $"<option value=\"{loginType}\">{innerText}</option>"; 27 } 28 }
Login 视图:
25 行中使用了刚建立的 LoginTypeTagHelper:
1 @model LoginModel 2 3 @{ 4 ViewData["Title"] = "Login"; 5 } 6 7 <h2>Login</h2> 8 <br/> 9 <div class="text-danger" asp-validation-summary="All"></div> 10 <br/> 11 <form asp-action="Login" method="post"> 12 <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl"/> 13 <div class="form-group"> 14 <label asp-for="Account"></label> 15 <input asp-for="Account" class="form-control" placeholder="请输入你的学号 / 邮箱 / 手机号"/> 16 </div> 17 <div class="form-group"> 18 <label asp-for="Password"></label> 19 <input asp-for="Password" class="form-control" placeholder="请输入你的密码"/> 20 </div> 21 <div class="form-group"> 22 <label>登录方式</label> 23 <select asp-for="LoginType"> 24 <option disabled value="">登录方式</option> 25 <LoginType login-type="@Enum.GetNames(typeof(LoginType))"></LoginType> 26 </select> 27 </div> 28 <input type="submit" class="btn btn-primary"/> 29 </form>
然后创建一个用于对信息进行验证的动作方法。
为了获取数据库的数据以及对数据进行验证授权,需要通过 DI(依赖注入) 获取对应的 UserManager 和 SignInManager 对象,在此针对 StudentAccountController 的构造函数进行更新。
StudentAccountController 整体:
1 [Authorize] 2 public class StudentAccountController : Controller 3 { 4 private UserManager<Student> _userManager; 5 private SignInManager<Student> _signInManager; 6 7 public StudentAccountController(UserManager<Student> studentManager, SignInManager<Student> signInManager) 8 { 9 _userManager = studentManager; 10 _signInManager = signInManager; 11 } 12 13 [AllowAnonymous] 14 public IActionResult Login(string returnUrl) 15 { 16 if (HttpContext.User.Identity.IsAuthenticated) 17 { 18 return RedirectToAction("AccountInfo"); 19 } 20 21 LoginModel loginInfo = new LoginModel(); 22 ViewBag.returnUrl = returnUrl; 23 return View(loginInfo); 24 } 25 26 [HttpPost] 27 [ValidateAntiForgeryToken] 28 [AllowAnonymous] 29 public async Task<IActionResult> Login(LoginModel loginInfo, string returnUrl) 30 { 31 if (ModelState.IsValid) 32 { 33 Student student =await GetStudentByLoginModel(loginInfo); 34 35 if (student == null) 36 { 37 return View(loginInfo); 38 } 39 SignInResult signInResult = await _signInManager.PasswordSignInAsync(student, loginInfo.Password, false, false); 40 41 if (signInResult.Succeeded) 42 { 43 return Redirect(returnUrl ?? "/StudentAccount/"+nameof(AccountInfo)); 44 } 45 46 ModelState.AddModelError("", "账号或密码错误"); 47 48 } 49 50 return View(loginInfo); 51 } 52 53 public IActionResult AccountInfo() 54 { 55 return View(CurrentAccountData()); 56 } 57 58 Dictionary<string, object> CurrentAccountData() 59 { 60 var userName = HttpContext.User.Identity.Name; 61 var user = _userManager.FindByNameAsync(userName).Result; 62 63 return new Dictionary<string, object>() 64 { 65 ["学号"]=userName, 66 ["姓名"]=user.Name, 67 ["邮箱"]=user.Email, 68 ["手机号"]=user.PhoneNumber, 69 }; 70 } 71 }
_userManager 以及 _signInManager 将通过 DI 获得实例;[ValidateAntiForgeryToken] 特性用于防止 XSRF 攻击;returnUrl 参数用于接收或返回之前正在访问的页面,在此处若 returnUrl 为空则返回 AccountInfo 页面;[Authorize] 特性用于确保只有已授权的用户才能访问对应动作方法;CurrentAccountData 方法用于获取当前用户的信息以在 AccountInfo 视图中呈现。
由于未进行授权,在此直接访问 AccountInfo 方法默认会返回 /Account/Login 页面请求验证,可通过在 Startup.cs 的 ConfigureServices 方法进行配置以覆盖这一行为,让页面默认返回 /StudentAccount/Login :
1 services.ConfigureApplicationCookie(opts => 2 { 3 opts.LoginPath = "/StudentAccount/Login"; 4 }
为了使 [Authorize] 特性能够正常工作,需要在 Configure 方法中使用 Authentication 中间件,如果没有调用 app.UseAuthentication(),则访问带有 [Authorize] 的方法会再度要求进行验证。中间件的顺序很重要:
1 app.UseAuthentication(); 2 app.UseHttpsRedirection(); 3 app.UseStaticFiles(); 4 app.UseCookiePolicy();
直接访问 AccountInfo 页面:
输入账号密码进行验证:
验证之后返回 /StudentAccount/AccountInfo 页面:
四、创建登出网页
简单地调用 SignOutAsync 用以清除当前 Cookie 中的授权信息。
1 public async Task<IActionResult> Logout(string returnUrl) 2 { 3 await _signInManager.SignOutAsync(); 4 if (returnUrl == null) 5 { 6 return View("Login"); 7 } 8 9 return Redirect(returnUrl); 10 }
同时在 AccountInfo 添加登出按钮:
1 @model Dictionary<string, object> 2 @{ 3 ViewData["Title"] = "AccountInfo"; 4 } 5 <h2>账户信息</h2> 6 <ul> 7 @foreach (var info in Model) 8 { 9 <li>@info.Key: @Model[info.Key]</li> 10 } 11 </ul> 12 <br /> 13 <a class="btn btn-danger" asp-action="Logout">登出</a>
登出后返回 Login 页面,同时 AccountInfo 页面需要重新进行验证。
附加使用邮箱以及手机号验证的测试:
五、基于 Role 的 Identity 授权
修改 StudentInitial 类,添加名为 admin 的学生数组并使用 AddToRoleAsync 为用户添加身份。在添加 Role 之前需要在 RoleManager 对象中使用 Create 方法为 Role 数据库添加特定的 Role 字段:
1 public class StudentInitiator 2 { 3 public static async Task InitialStudents(IServiceProvider serviceProvider) 4 { 5 UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>(); 6 RoleManager<IdentityRole> roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>(); 7 if (userManager.Users.Any()) 8 { 9 return; 10 } 11 12 if (await roleManager.FindByNameAsync("Admin")==null) 13 { 14 await roleManager.CreateAsync(new IdentityRole("Admin")); 15 } 16 17 if (await roleManager.FindByNameAsync("Student")==null) 18 { 19 await roleManager.CreateAsync(new IdentityRole("Student")); 20 } 21 22 IEnumerable<Student> initialStudents = new[] 23 { 24 new Student() 25 { 26 UserName = "U201600001", 27 Name = "Nanase", 28 Email = "Nanase@cnblog.com", 29 PhoneNumber = "12345678910", 30 Degree = Degrees.CollegeStudent, 31 MaxBooksNumber = 10, 32 }, 33 new Student() 34 { 35 UserName = "U201600002", 36 Name = "Ruri", 37 Email = "NanaseRuri@cnblog.com", 38 PhoneNumber = "12345678911", 39 Degree = Degrees.DoctorateDegree, 40 MaxBooksNumber = 15 41 } 42 }; 43 44 IEnumerable<Student> initialAdmins = new[] 45 { 46 new Student() 47 { 48 UserName = "A000000000", 49 Name="Admin0000", 50 Email = "Admin@cnblog.com", 51 PhoneNumber = "12345678912", 52 Degree = Degrees.CollegeStudent, 53 MaxBooksNumber = 20 54 }, 55 new Student() 56 { 57 UserName = "A000000001", 58 Name = "Admin0001", 59 Email = "123456789@qq.com", 60 PhoneNumber = "12345678910", 61 Degree = Degrees.CollegeStudent, 62 MaxBooksNumber = 20 63 }, 64 }; 65 foreach (var student in initialStudents) 66 { 67 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6, 6)); 68 } 69 foreach (var admin in initialAdmins) 70 { 71 await userManager.CreateAsync(admin, "zxcZXC!123"); 72 await userManager.AddToRoleAsync(admin, "Admin"); 73 } 74 } 75 }
对 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 的身份才可以访问该控制器。
1 [Authorize(Roles = "Admin")] 2 public class AdminAccountController : Controller 3 { 4 private UserManager<Student> _userManager; 5 6 public AdminAccountController(UserManager<Student> userManager) 7 { 8 _userManager = userManager; 9 } 10 11 public IActionResult Index() 12 { 13 ICollection<Student> students = _userManager.Users.ToList(); 14 return View(students); 15 } 16 }
Index 视图:
1 @using LibraryDemo.Models.DomainModels 2 @model IEnumerable<LibraryDemo.Models.DomainModels.Student> 3 @{ 4 ViewData["Title"] = "AccountInfo"; 5 Student stu = new Student(); 6 } 7 <link rel="stylesheet" href="~/css/BookInfo.css" /> 8 9 <script> 10 function confirmDelete() { 11 var userNames = document.getElementsByName("userNames"); 12 var message = "确认删除"; 13 var values = []; 14 for (i in userNames) { 15 if (userNames[i].checked) { 16 message = message + userNames[i].value+","; 17 values.push(userNames[i].value); 18 } 19 } 20 message = message + "?"; 21 if (confirm(message)) { 22 $.ajax({ 23 url: "@Url.Action("RemoveStudent")", 24 contentType: "application/json", 25 method: "POST", 26 data: JSON.stringify(values), 27 success: function(students) { 28 updateTable(students); 29 } 30 }); 31 } 32 } 33 34 function updateTable(data) { 35 var body = $("#studentList"); 36 body.empty(); 37 for (var i = 0; i < data.length; i++) { 38 var person = data[i]; 39 body.append(`<tr><td><input type="checkbox" name="userNames" value="${person.userName}" /></td> 40 <td>${person.userName}</td><td>${person.name}</td><td>${person.degree}</td> 41 <td>${person.phoneNumber}</td><td>${person.email}</td><td>${person.maxBooksNumber}</td></tr>`); 42 } 43 }; 44 45 function addStudent() { 46 var studentList = $("#studentList"); 47 if (!document.getElementById("studentInfo")) { 48 studentList.append('<tr id="studentInfo">' + 49 '<td></td>' + 50 '<td><input type="text" name="UserName" id="UserName" /></td>' + 51 '<td><input type="text" name="Name" id="Name" /></td>' + 52 '<td><input type="text" name="Degree" id="Degree" /></td>' + 53 '<td><input type="text" name="PhoneNumber" id="PhoneNumber" /></td>' + 54 '<td><input type="text" name="Email" id="Email" /></td>' + 55 '<td><input type="text" name="MaxBooksNumber" id="MaxBooksNumber" /></td>' + 56 '<td><button type="submit" onclick="return postAddStudent()">添加</button></td>' + 57 '</tr>'); 58 } 59 } 60 61 function postAddStudent() { 62 $.ajax({ 63 url: "@Url.Action("AddStudent")", 64 contentType: "application/json", 65 method: "POST", 66 data: JSON.stringify({ 67 UserName: $("#UserName").val(), 68 Name: $("#Name").val(), 69 Degree:$("#Degree").val(), 70 PhoneNumber: $("#PhoneNumber").val(), 71 Email: $("#Email").val(), 72 MaxBooksNumber: $("#MaxBooksNumber").val() 73 }), 74 success: function (student) { 75 addStudentToTable(student); 76 } 77 }); 78 } 79 80 function addStudentToTable(student) { 81 var studentList = document.getElementById("studentList"); 82 var studentInfo = document.getElementById("studentInfo"); 83 studentList.removeChild(studentInfo); 84 85 $("#studentList").append(`<tr>` + 86 `<td><input type="checkbox" name="userNames" value="${student.userName}" /></td>` + 87 `<td>${student.userName}</td>` + 88 `<td>${student.name}</td>`+ 89 `<td>${student.degree}</td>` + 90 `<td>${student.phoneNumber}</td>` + 91 `<td>${student.email}</td>` + 92 `<td>${student.maxBooksNumber}</td >` + 93 `</tr>`); 94 } 95 </script> 96 97 <h2>学生信息</h2> 98 99 <div id="buttonGroup"> 100 <button class="btn btn-primary" onclick="return addStudent()">添加学生</button> 101 <button class="btn btn-danger" onclick="return confirmDelete()">删除学生</button> 102 </div> 103 104 105 <br /> 106 <table> 107 <thead> 108 <tr> 109 <th></th> 110 <th>@Html.LabelFor(m => stu.UserName)</th> 111 <th>@Html.LabelFor(m => stu.Name)</th> 112 <th>@Html.LabelFor(m => stu.Degree)</th> 113 <th>@Html.LabelFor(m => stu.PhoneNumber)</th> 114 <th>@Html.LabelFor(m => stu.Email)</th> 115 <th>@Html.LabelFor(m => stu.MaxBooksNumber)</th> 116 </tr> 117 </thead> 118 <tbody id="studentList"> 119 120 @if (!@Model.Any()) 121 { 122 <tr><td colspan="6">未有学生信息</td></tr> 123 } 124 else 125 { 126 foreach (var student in Model) 127 { 128 <tr> 129 <td><input type="checkbox" name="userNames" value="@student.UserName" /></td> 130 <td>@student.UserName</td> 131 <td>@student.Name</td> 132 <td>@Html.DisplayFor(m => student.Degree)</td> 133 <td>@student.PhoneNumber</td> 134 <td>@student.Email</td> 135 <td>@student.MaxBooksNumber</td> 136 </tr> 137 } 138 } 139 </tbody> 140 </table>
使用 Role 不是 Admin 的账户登录:
使用 Role 为 Admin 的账户登录: