您的位置:首页 > 编程语言 > ASP

ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立

2019-02-14 00:03 1596 查看

前言:

本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。

本系列文章主要参考资料:

微软文档: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 的账户登录:

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐