==>>点击查看本系列文章目录
目录
1.创建项目
2.重写用户管理界面
3.用户管理
4.用户注册
5.用户登录
6.用户退出
7.自动数据迁移
8.启动应用
1.创建项目
项目中会多出如下红框内的用户身份管理的文件:
单独启动该项目,已经拥有了用户登录、注册、注销、管理等功能页面。
2.重写用户管理界面
但是,我们并不想使用原有的登录页面,我们想要自定义用户页面。
项目=》 右键 =》 添加 =》 新搭建基架的项目
添加过程中如果报错 “运行所选代码生成器时出错”, 则先清理该项目,尝试重新添加,如果仍然报错,重启VSStudio,再次添加基架即可。
然后就会看到项目中添加了下图中的文件:
User.cs 中添加姓名和生日:
public class User : IdentityUser { [PersonalData] public string Name { get; set; } [PersonalData] public DateTime Birthday { get; set; } }
3.用户管理
Leo.Users.Areas.Identity.Pages.Account.Manage.Index.cshtml.cs :
public partial class IndexModel : PageModel { private readonly UserManager<User> _userManager; private readonly SignInManager<User> _signInManager; private readonly IEmailSender _emailSender; public IndexModel( UserManager<User> userManager, SignInManager<User> signInManager, IEmailSender emailSender) { _userManager = userManager; _signInManager = signInManager; _emailSender = emailSender; } public string Username { get; set; } public bool IsEmailConfirmed { get; set; } [TempData] public string StatusMessage { get; set; } [BindProperty] public InputModel Input { get; set; } public class InputModel { #region 新增 [Required] [DataType(DataType.Text)] [Display(Name = "姓名")] public string Name { get; set; } [Required] [Display(Name = "生日")] [DataType(DataType.Date)] public DateTime Birthday { get; set; } #endregion [Required] [EmailAddress] public string Email { get; set; } [Phone] [Display(Name = "电话")] public string PhoneNumber { get; set; } } public async Task<IActionResult> OnGetAsync() { var user = await _userManager.GetUserAsync(User); if (user == null) { return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var userName = await _userManager.GetUserNameAsync(user); var email = await _userManager.GetEmailAsync(user); var phoneNumber = await _userManager.GetPhoneNumberAsync(user); Username = userName; Input = new InputModel { #region 新增 Name = user.Name, Birthday = user.Birthday, #endregion Email = email, PhoneNumber = phoneNumber }; IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); return Page(); } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } var user = await _userManager.GetUserAsync(User); if (user == null) { return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var email = await _userManager.GetEmailAsync(user); if (Input.Email != email) { var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email); if (!setEmailResult.Succeeded) { var userId = await _userManager.GetUserIdAsync(user); throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'."); } } #region 新增 if (Input.Name != user.Name) { user.Name = Input.Name; } if (Input.Birthday != user.Birthday) { user.Birthday = Input.Birthday; } #endregion var phoneNumber = await _userManager.GetPhoneNumberAsync(user); if (Input.PhoneNumber != phoneNumber) { var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber); if (!setPhoneResult.Succeeded) { var userId = await _userManager.GetUserIdAsync(user); throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{userId}'."); } } #region 新增 await _userManager.UpdateAsync(user); #endregion await _signInManager.RefreshSignInAsync(user); StatusMessage = "Your profile has been updated"; return RedirectToPage(); } public async Task<IActionResult> OnPostSendVerificationEmailAsync() { if (!ModelState.IsValid) { return Page(); } var user = await _userManager.GetUserAsync(User); if (user == null) { return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var userId = await _userManager.GetUserIdAsync(user); var email = await _userManager.GetEmailAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new { userId = userId, code = code }, protocol: Request.Scheme); await _emailSender.SendEmailAsync( email, "Confirm your email", $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>."); StatusMessage = "Verification email sent. Please check your email."; return RedirectToPage(); } }
展示页面 Index.cshtml :
@page @model IndexModel @{ ViewData["Title"] = "Profile"; ViewData["ActivePage"] = ManageNavPages.Index; } <h4>@ViewData["Title"]</h4> <partial name="_StatusMessage" for="StatusMessage" /> <div class="row"> <div class="col-md-6"> <form id="profile-form" method="post"> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Username"></label> <input asp-for="Username" class="form-control" disabled /> </div> <div class="form-group"> <label asp-for="Input.Email"></label> @if (Model.IsEmailConfirmed) { <div class="input-group"> <input asp-for="Input.Email" class="form-control" /> <span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span> </div> } else { <input asp-for="Input.Email" class="form-control" /> <button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button> } <span asp-validation-for="Input.Email" class="text-danger"></span> </div> <div class="form-group"> @* 新增 start *@ <div class="form-group"> <label asp-for="Input.Name"></label> <input asp-for="Input.Name" class="form-control" /> </div> <div class="form-group"> <label asp-for="Input.Birthday"></label> <input asp-for="Input.Birthday" class="form-control" /> </div> @* end *@ <label asp-for="Input.PhoneNumber"></label> <input asp-for="Input.PhoneNumber" class="form-control" /> <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span> </div> <button id="update-profile-button" type="submit" class="btn btn-primary">Save</button> </form> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
4.用户注册
Leo.Users.Areas.Identity.Pages.Account.Register.cshtml.cs :
[AllowAnonymous] public class RegisterModel : PageModel { private readonly SignInManager<User> _signInManager; private readonly UserManager<User> _userManager; private readonly ILogger<RegisterModel> _logger; private readonly IEmailSender _emailSender; public RegisterModel( UserManager<User> userManager, SignInManager<User> signInManager, ILogger<RegisterModel> logger, IEmailSender emailSender) { _userManager = userManager; _signInManager = signInManager; _logger = logger; _emailSender = emailSender; } [BindProperty] public InputModel Input { get; set; } public string ReturnUrl { get; set; } public class InputModel { #region 新增 [Required] [DataType(DataType.Text)] [Display(Name = "姓名")] public string Name { get; set; } [Required] [Display(Name = "生日")] [DataType(DataType.Date)] public DateTime Birthday { get; set; } #endregion [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 = "密码")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "确认密码")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } public void OnGet(string returnUrl = null) { ReturnUrl = returnUrl; } public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); if (ModelState.IsValid) { var user = new User { #region 新增 Name = Input.Name, Birthday = Input.Birthday, #endregion UserName = Input.Email, Email = Input.Email }; 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); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new { userId = user.Id, code = code }, 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>."); await _signInManager.SignInAsync(user, isPersistent: false); return LocalRedirect(returnUrl); } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } // If we got this far, something failed, redisplay form return Page(); } }
展示页面 Register.cshtml :
@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> @* 新增 start *@ <div class="form-group"> <label asp-for="Input.Name"></label> <input asp-for="Input.Name" class="form-control" /> <span asp-validation-for="Input.Name" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.Birthday"></label> <input asp-for="Input.Birthday" class="form-control" /> <span asp-validation-for="Input.Birthday" class="text-danger"></span> </div> @* end *@ <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"> <label asp-for="Input.ConfirmPassword"></label> <input asp-for="Input.ConfirmPassword" class="form-control" /> <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">Register</button> </form> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
5.用户登录
Login.cshtml.cs :
[AllowAnonymous] public class LoginModel : PageModel { private readonly SignInManager<User> _signInManager; private readonly ILogger<LoginModel> _logger; public LoginModel(SignInManager<User> signInManager, ILogger<LoginModel> logger) { _signInManager = signInManager; _logger = logger; } [BindProperty] public InputModel Input { get; set; } public IList<AuthenticationScheme> ExternalLogins { get; set; } public string ReturnUrl { get; set; } [TempData] public string ErrorMessage { get; set; } public class InputModel { [Required] [EmailAddress] public string Email { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } public async Task OnGetAsync(string returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { ModelState.AddModelError(string.Empty, ErrorMessage); } returnUrl = returnUrl ?? Url.Content("~/"); // Clear the existing external cookie to ensure a clean login process await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ReturnUrl = returnUrl; } public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); if (ModelState.IsValid) { // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); if (result.Succeeded) { _logger.LogInformation("User logged in."); return LocalRedirect(returnUrl); } if (result.RequiresTwoFactor) { return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); } if (result.IsLockedOut) { _logger.LogWarning("User account locked out."); return RedirectToPage("./Lockout"); } else { ModelState.AddModelError(string.Empty, "Invalid login attempt."); return Page(); } } // If we got this far, something failed, redisplay form return Page(); } }
Login.cshtml :
@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> </div> </form> </section> </div> <div class="col-md-6 col-md-offset-2"> <section> <h4>Use another service to log in.</h4> <hr /> @{ if ((Model.ExternalLogins?.Count ?? 0) == 0) { <div> <p> There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a> for details on setting up this ASP.NET application to support logging in via external services. </p> </div> } else { <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> <div> <p> @foreach (var provider in Model.ExternalLogins) { <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> } </p> </div> </form> } } </section> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
6.用户退出
Logout.cshtml.cs :
[AllowAnonymous] public class LogoutModel : PageModel { private readonly SignInManager<User> _signInManager; private readonly ILogger<LogoutModel> _logger; public LogoutModel(SignInManager<User> signInManager, ILogger<LogoutModel> logger) { _signInManager = signInManager; _logger = logger; } public void OnGet() { } public async Task<IActionResult> OnPost(string returnUrl = null) { await _signInManager.SignOutAsync(); _logger.LogInformation("User logged out."); if (returnUrl != null) { return LocalRedirect(returnUrl); } else { return Page(); } } }
Logout.cshtml :
@page @model LogoutModel @{ ViewData["Title"] = "Log out"; } <header> <h1>@ViewData["Title"]</h1> <p>您已成功退出</p> </header>
7.自动数据迁移
程序自动完成数据库以及表的构建
如图,会报错,是因为项目中有两个Context 数据上下文:
删掉下面的包含 ApplicationDbContext.cs 的 Data文件夹
然后编译时startup.cs 会报错,找不到ApplicationDbContext 类,此时我们直接将这一段注释掉即可,因为我们添加基架以后已经有了新的数据模块:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); #region 身份认证相关,注释掉 //services.AddDbContext<ApplicationDbContext>(options => // options.UseSqlServer( // Configuration.GetConnectionString("DefaultConnection"))); //services.AddDefaultIdentity<IdentityUser>() // .AddDefaultUI(UIFramework.Bootstrap4) // .AddEntityFrameworkStores<ApplicationDbContext >(); #endregion services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); } else { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(); #region 身份认证相关,保留 app.UseAuthentication(); #endregion app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } }
再次添加迁移,不再报错:
Add-Migration CustomUserData
更新到数据库:
Update-Database
完成。
查看数据库:
为了防止以后出现问题,我们删掉原来的DefaultConnection ,将 UsersContextConnection 改为 DefaultConnection, 搜索“UsersContextConnection” ,用到该字符串的地方都替换为 “DefaultConnection” 。
8.启动应用
选择用 Kestrel 服务器启动,方便监控:
启动以后,会抛出异常( InvalidOperationException: No service for type 'Microsoft.AspNetCore.Identity.UserManager`1[Microsoft.AspNetCore.Identity.IdentityUser]' has been registered.):
意思是说 UserManager<IdentityUser> 类型的服务没有被注册,事实上,我们将IdentityUser实现为User ,是对User模型进行管理,搜索 IdentityUser,替换为 User
记得添加命名空间,不然引入的User并不正确:
@inject SignInManager<Leo.Users.Areas.Identity.Data.User> SignInManager @inject UserManager<Leo.Users.Areas.Identity.Data.User> UserManager
启动成功,注册账号:
完成:
接下来,我们检查一下后台的用户数据:
完成!