- 通用
- 身份验证
- 授权
- 数据保护
- 机密管理
添加、 下载和删除标识到 ASP.NET Core项目中的自定义用户数据
本文介绍如何:
- 将自定义用户数据添加到 ASP.NET Core web 应用程序。
- 使用 PersonalDataAttribute 特性标记自定义用户数据模型,使其自动可供下载和删除。 使能够下载和删除数据可帮助满足GDPR要求。
项目示例将创建从 Razor 页 web 应用,但了 ASP.NET Core MVC web 应用的类似的说明。
先决条件
创建 Razor Web 应用
- Visual Studio
-
.NET Core CLI
dotnet new webapp -o WebApp1
运行标识基架
-
Visual Studio
- 从解决方案资源管理器,右键单击该项目 >添加 > 新基架项。
- 在 "添加基架" 对话框的左窗格中,选择 "标识" > "添加"。
- 在 "添加标识" 对话框中,选择以下选项:
- 选择现有的布局文件 ~/Pages/Shared/_Layout.cshtml
- 选择要重写的以下文件:
- 帐户/注册
- 帐户/管理/索引
- 选择 + 按钮以创建一个新数据上下文类。 接受的类型 (WebApp1.Models.WebApp1Context如果项目命名为WebApp1)。
- 选择 + 按钮以创建一个新User 类。 接受的类型 (WebApp1User如果项目命名为WebApp1) >添加。
- 选择“添加”。
-
.NET Core CLI
如果以前未安装 ASP.NET Core 基架,请立即进行安装:
dotnet tool install -g dotnet-aspnet-codegenerator
添加到包引用Microsoft.VisualStudio.Web.CodeGeneration.Design项目 (.csproj) 文件。 在项目目录中运行以下命令:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet restore
运行以下命令以列出标识基架选项:
dotnet aspnet-codegenerator identity -h
在项目文件夹中,运行标识基架:
dotnet aspnet-codegenerator identity -u WebApp1User -fi Account.Register;Account.Manage.Index
按照中的说明迁移、 UseAuthentication 和布局来执行以下步骤:
- 创建迁移并更新数据库。
- 将
UseAuthentication
添加到Startup.Configure
。 - 添加
<partial name="_LoginPartial" />
布局文件。 - 测试应用:
- 注册用户
- 选择新的用户名称 (旁边注销链接)。 您可能需要展开窗口或选择要显示的用户名称和其他链接的导航栏图标。
- 选择个人数据选项卡。
- 选择下载按钮,然后检查PersonalData.json文件。
- 测试删除按钮,删除已登录用户。
向标识数据库中添加自定义用户数据
更新IdentityUser
派生类使用自定义属性。 如果项目 WebApp1 命名为,将该文件命名Areas/Identity/Data/WebApp1User.cs。 使用以下代码更新文件:
using System; using Microsoft.AspNetCore.Identity; namespace WebApp1.Areas.Identity.Data { public class WebApp1User : IdentityUser { [PersonalData] public string Name { get; set; } [PersonalData] public DateTime DOB { get; set; } } }
using Microsoft.AspNetCore.Identity; using System; namespace WebApp1.Areas.Identity.Data { public class WebApp1User : IdentityUser { [PersonalData] public string Name { get; set; } [PersonalData] public DateTime DOB { get; set; } } }
具有PersonalData属性的属性为:
- 时删除Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml Razor 页面调用
UserManager.Delete
。 - 通过下载的数据中包含Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml Razor 页面。
更新 Account/Manage/Index.cshtml 页
更新InputModel
中Areas/Identity/Pages/Account/Manage/Index.cshtml.cs用以下突出显示的代码:
public partial class IndexModel : PageModel { private readonly UserManager<WebApp1User> _userManager; private readonly SignInManager<WebApp1User> _signInManager; public IndexModel( UserManager<WebApp1User> userManager, SignInManager<WebApp1User> signInManager) { _userManager = userManager; _signInManager = signInManager; } public string Username { get; set; } [TempData] public string StatusMessage { get; set; } [BindProperty] public InputModel Input { get; set; } public class InputModel { [Required] [DataType(DataType.Text)] [Display(Name = "Full name")] public string Name { get; set; } [Required] [Display(Name = "Birth Date")] [DataType(DataType.Date)] public DateTime DOB { get; set; } [Phone] [Display(Name = "Phone number")] public string PhoneNumber { get; set; } } private async Task LoadAsync(WebApp1User user) { var userName = await _userManager.GetUserNameAsync(user); var phoneNumber = await _userManager.GetPhoneNumberAsync(user); Username = userName; Input = new InputModel { Name = user.Name, DOB = user.DOB, PhoneNumber = phoneNumber }; } 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)}'."); } await LoadAsync(user); return Page(); } public async Task<IActionResult> OnPostAsync() { var user = await _userManager.GetUserAsync(User); if (user == null) { return NotFound( $"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } if (!ModelState.IsValid) { await LoadAsync(user); return Page(); } 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}'."); } } if (Input.Name != user.Name) { user.Name = Input.Name; } if (Input.DOB != user.DOB) { user.DOB = Input.DOB; } await _userManager.UpdateAsync(user); await _signInManager.RefreshSignInAsync(user); StatusMessage = "Your profile has been updated"; return RedirectToPage(); } }
更新Areas/Identity/Pages/Account/Manage/Index.cshtml与以下突出显示的标记:
@page @model IndexModel @{ ViewData["Title"] = "Profile"; ViewData["ActivePage"] = ManageNavPages.Index; } <h4>@ViewData["Title"]</h4> <partial name="_StatusMessage" model="Model.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.Name"></label> <input asp-for="Input.Name" class="form-control" /> </div> <div class="form-group"> <label asp-for="Input.DOB"></label> <input asp-for="Input.DOB" class="form-control" /> </div> <div class="form-group"> <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" /> }
public partial class IndexModel : PageModel { private readonly UserManager<WebApp1User> _userManager; private readonly SignInManager<WebApp1User> _signInManager; private readonly IEmailSender _emailSender; public IndexModel( UserManager<WebApp1User> userManager, SignInManager<WebApp1User> 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 { [Required] [DataType(DataType.Text)] [Display(Name = "Full name")] public string Name { get; set; } [Required] [Display(Name = "Birth Date")] [DataType(DataType.Date)] public DateTime DOB { get; set; } [Required] [EmailAddress] public string Email { get; set; } [Phone] [Display(Name = "Phone number")] 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 { Name = user.Name, DOB = user.DOB, 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}'."); } } if (Input.Name != user.Name) { user.Name = Input.Name; } if (Input.DOB != user.DOB) { user.DOB = Input.DOB; } 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}'."); } } await _userManager.UpdateAsync(user); 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(); } }
更新Areas/Identity/Pages/Account/Manage/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"> <label asp-for="Input.Name"></label> <input asp-for="Input.Name" class="form-control" /> </div> <div class="form-group"> <label asp-for="Input.DOB"></label> <input asp-for="Input.DOB" class="form-control" /> </div> <div class="form-group"> <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" /> }
更新 account/Register.cshtml 页面
更新InputModel
中Areas/Identity/Pages/Account/Register.cshtml.cs用以下突出显示的代码:
[AllowAnonymous] public class RegisterModel : PageModel { private readonly SignInManager<WebApp1User> _signInManager; private readonly UserManager<WebApp1User> _userManager; private readonly ILogger<RegisterModel> _logger; private readonly IEmailSender _emailSender; public RegisterModel( UserManager<WebApp1User> userManager, SignInManager<WebApp1User> 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 IList<AuthenticationScheme> ExternalLogins { get; set; } public class InputModel { [Required] [DataType(DataType.Text)] [Display(Name = "Full name")] public string Name { get; set; } [Required] [Display(Name = "Birth Date")] [DataType(DataType.Date)] public DateTime DOB { 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 OnGetAsync(string returnUrl = null) { ReturnUrl = returnUrl; ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); } public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl = returnUrl ?? Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { var user = new WebApp1User { Name = Input.Name, DOB = Input.DOB, 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); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new { area = "Identity", 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>."); if (_userManager.Options.SignIn.RequireConfirmedAccount) { return RedirectToPage("RegisterConfirmation", new { email = Input.Email }); } else { 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(); } }
更新Areas/Identity/Pages/Account/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> <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.DOB"></label> <input asp-for="Input.DOB" class="form-control" /> <span asp-validation-for="Input.DOB" 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> <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 class="col-md-6 col-md-offset-2"> <section> <h4>Use another service to register.</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" /> }
[AllowAnonymous] public class RegisterModel : PageModel { private readonly SignInManager<WebApp1User> _signInManager; private readonly UserManager<WebApp1User> _userManager; private readonly ILogger<RegisterModel> _logger; private readonly IEmailSender _emailSender; public RegisterModel( UserManager<WebApp1User> userManager, SignInManager<WebApp1User> 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 { [Required] [DataType(DataType.Text)] [Display(Name = "Full name")] public string Name { get; set; } [Required] [Display(Name = "Birth Date")] [DataType(DataType.Date)] public DateTime DOB { 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 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 WebApp1User { Name = Input.Name, DOB = Input.DOB, 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(); } }
更新Areas/Identity/Pages/Account/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> <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.DOB"></label> <input asp-for="Input.DOB" class="form-control" /> <span asp-validation-for="Input.DOB" 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> <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" /> }
生成此项目。
添加自定义用户数据的迁移
-
Visual Studio
在 Visual Studio程序包管理器控制台:
Add-Migration CustomUserData Update-Database
-
.NET Core CLI
dotnet ef migrations add CustomUserData dotnet ef database update
测试创建、 查看、 下载和删除自定义用户数据
测试应用:
- 注册一个新用户。
- 查看自定义用户数据
/Identity/Account/Manage
页。 - 下载并查看用户个人数据从
/Identity/Account/Manage/PersonalData
页。