ASP.NET Core 프로젝트에서 Identity에 사용자 지정 사용자 데이터 추가, 다운로드 및 삭제
작성자: Rick Anderson
이 문서는 다음 방법을 안내합니다.
- ASP.NET Core 웹앱에 사용자 지정 사용자 데이터를 추가합니다.
- 사용자 지정 사용자 데이터 모델을 PersonalDataAttribute 특성으로 표시하여 다운로드 및 삭제에 자동으로 사용할 수 있도록 합니다. 데이터를 다운로드하고 삭제할 수 있도록 하면 GDPR 요구 사항을 충족하는 데 도움이 됩니다.
프로젝트 샘플은 Razor Pages 웹앱에서 생성되지만 지침은 ASP.NET Core MVC 웹앱에 대해 유사합니다.
필수 조건
Razor 웹앱 만들기
- Visual Studio 파일 메뉴에서 새로 만들기>프로젝트를 선택합니다. 다운로드 샘플 코드의 네임스페이스와 일치하려면 프로젝트 이름을 WebApp1로 지정합니다.
- ASP.NET Core 웹 애플리케이션>확인을 선택합니다.
- 웹 애플리케이션>확인을 선택합니다.
- 프로젝트를 빌드하고 실행합니다.
Identity 스캐폴더 실행
- 솔루션 탐색기에서 프로젝트 > >추가>새 스캐폴드 항목을 마우스 오른쪽 단추로 클릭합니다.
- 스캐폴드 추가 대화 상자의 왼쪽 창에서 Identity>추가를 선택합니다.
- 추가 Identity 대화 상자에서 다음 옵션을 선택합니다.
- 기존 레이아웃 파일
~/Pages/Shared/_Layout.cshtml
을 선택합니다. - 재정의할 다음 파일을 선택합니다.
- Account/Register
- Account/Manage/Index
- + 단추를 선택하여 새 데이터 컨텍스트 클래스를 만듭니다. 형식(프로젝트 이름이 WebApp1인 경우 WebApp1.Models.WebApp1Context)을 적용합니다.
- + 단추를 선택하여 새 사용자 클래스를 만듭니다. 형식(프로젝트 이름이 WebApp1인 경우 WebApp1User) >추가를 적용합니다.
- 기존 레이아웃 파일
- 추가를 선택합니다.
마이그레이션, UseAuthentication 및 레이아웃의 지침에 따라 다음 단계를 수행합니다.
- 마이그레이션을 만들고 데이터베이스를 업데이트합니다.
- 에 추가
UseAuthentication
Program.cs
- 레이아웃 파일에
<partial name="_LoginPartial" />
을 추가합니다. - 앱을 테스트합니다.
- 사용자 등록
- 새 사용자 이름(로그아웃 링크 옆)을 선택합니다. 창을 확장하거나 탐색 모음 아이콘을 선택하여 사용자 이름 및 기타 링크를 표시해야 할 수 있습니다.
- 개인 데이터 탭을 선택합니다.
- 다운로드 단추를 선택하고 파일을 검사했습니다
PersonalData.json
. - 로그온한 사용자를 삭제하는 삭제 단추를 테스트합니다.
사용자 지정 사용자 데이터를 Identity DB에 추가
IdentityUser
파생 클래스를 사용자 지정 속성으로 업데이트합니다. WebApp1 프로젝트의 이름을 지정한 경우 파일 이름은 Areas/Identity/Data/WebApp1User.cs
입니다. 파일을 다음 코드로 업데이트합니다.
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; }
}
PersonalData 특성이 있는 속성은 다음과 같습니다.
Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml
Razor페이지에서UserManager.Delete
호출할 때 삭제됩니다.- 다운로드한 데이터에는
Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml
Razor 페이지가 포함됩니다.
Account/Manage/Index.cshtml
페이지 업데이트
다음 강조 표시된 코드로 Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
에서 InputModel
을 업데이트합니다.
public 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;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Username { get; set; }
// Remaining API warnings ommited.
[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)
{
StatusMessage = "Unexpected error when trying to set phone number.";
return RedirectToPage();
}
}
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;
}
<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="profile-form" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-floating">
<input asp-for="Username" class="form-control" disabled />
<label asp-for="Username" class="form-label"></label>
</div>
<div class="form-floating">
<input asp-for="Input.Name" class="form-control" />
<label asp-for="Input.Name" class="form-label"></label>
</div>
<div class="form-floating">
<input asp-for="Input.DOB" class="form-control" />
<label asp-for="Input.DOB" class="form-label"></label>
</div>
<div class="form-floating">
<input asp-for="Input.PhoneNumber" class="form-control" />
<label asp-for="Input.PhoneNumber" class="form-label"></label>
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
</div>
<button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Account/Register.cshtml
페이지 업데이트
다음 강조 표시된 코드로 Areas/Identity/Pages/Account/Register.cshtml.cs
에서 InputModel
을 업데이트합니다.
public class RegisterModel : PageModel
{
private readonly SignInManager<WebApp1User> _signInManager;
private readonly UserManager<WebApp1User> _userManager;
private readonly IUserStore<WebApp1User> _userStore;
private readonly IUserEmailStore<WebApp1User> _emailStore;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager<WebApp1User> userManager,
IUserStore<WebApp1User> userStore,
SignInManager<WebApp1User> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender)
{
_userManager = userManager;
_userStore = userStore;
_emailStore = GetEmailStore();
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
// Remaining API warnings ommited.
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 ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
var user = CreateUser();
user.Name = Input.Name;
user.DOB = Input.DOB;
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var userId = await _userManager.GetUserIdAsync(user);
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 = userId, code = code, returnUrl = returnUrl },
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, returnUrl = returnUrl });
}
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();
}
private WebApp1User CreateUser()
{
try
{
return Activator.CreateInstance<WebApp1User>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(WebApp1User)}'. " +
$"Ensure that '{nameof(WebApp1User)}' is not an abstract class and has a parameterless constructor, or alternatively " +
$"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
}
}
private IUserEmailStore<WebApp1User> GetEmailStore()
{
if (!_userManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<WebApp1User>)_userStore;
}
}
}
다음 강조 표시된 코드로 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 id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<h2>Create a new account.</h2>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-floating">
<input asp-for="Input.Name" class="form-control" />
<label asp-for="Input.Name"></label>
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="Input.DOB" class="form-control" />
<label asp-for="Input.DOB"></label>
<span asp-validation-for="Input.DOB" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
<label asp-for="Input.Email"></label>
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
<label asp-for="Input.Password"></label>
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
<label asp-for="Input.ConfirmPassword"></label>
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</form>
</div>
<div class="col-md-6 col-md-offset-2">
<section>
<h3>Use another service to register.</h3>
<hr />
@{
if ((Model.ExternalLogins?.Count ?? 0) == 0)
{
<div>
<p>
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
about setting up this ASP.NET application to support logging in via external services</a>.
</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" />
}
프로젝트를 빌드합니다.
레이아웃 업데이트
모든 페이지에 로그인 및 로그아웃 링크를 추가하는 방법은 레이아웃 변경을 참조하세요.
사용자 지정 사용자 데이터에 대한 마이그레이션 추가
Visual Studio 패키지 관리자 콘솔에서 다음을 실행합니다.
Add-Migration CustomUserData
Update-Database
사용자 지정 사용자 데이터 만들기, 보기, 다운로드, 삭제 테스트
앱을 테스트합니다.
- 새 사용자를 등록합니다.
/Identity/Account/Manage
페이지에서 사용자 지정 사용자 데이터를 봅니다./Identity/Account/Manage/PersonalData
페이지에서 사용자 개인 데이터를 다운로드하고 봅니다.
Razor 웹앱 만들기
- Visual Studio 파일 메뉴에서 새로 만들기>프로젝트를 선택합니다. 다운로드 샘플 코드의 네임스페이스와 일치하려면 프로젝트 이름을 WebApp1로 지정합니다.
- ASP.NET Core 웹 애플리케이션>확인을 선택합니다.
- 웹 애플리케이션>확인을 선택합니다.
- 프로젝트를 빌드하고 실행합니다.
Identity 스캐폴더 실행
- 솔루션 탐색기에서 프로젝트 > >추가>새 스캐폴드 항목을 마우스 오른쪽 단추로 클릭합니다.
- 스캐폴드 추가 대화 상자의 왼쪽 창에서 Identity>추가를 선택합니다.
- 추가 Identity 대화 상자에서 다음 옵션을 선택합니다.
- 기존 레이아웃 파일
~/Pages/Shared/_Layout.cshtml
을 선택합니다. - 재정의할 다음 파일을 선택합니다.
- Account/Register
- Account/Manage/Index
- + 단추를 선택하여 새 데이터 컨텍스트 클래스를 만듭니다. 형식(프로젝트 이름이 WebApp1인 경우 WebApp1.Models.WebApp1Context)을 적용합니다.
- + 단추를 선택하여 새 사용자 클래스를 만듭니다. 형식(프로젝트 이름이 WebApp1인 경우 WebApp1User) >추가를 적용합니다.
- 기존 레이아웃 파일
- 추가를 선택합니다.
마이그레이션, UseAuthentication 및 레이아웃의 지침에 따라 다음 단계를 수행합니다.
- 마이그레이션을 만들고 데이터베이스를 업데이트합니다.
UseAuthentication
를Startup.Configure
에 추가합니다.- 레이아웃 파일에
<partial name="_LoginPartial" />
을 추가합니다. - 앱을 테스트합니다.
- 사용자 등록
- 새 사용자 이름(로그아웃 링크 옆)을 선택합니다. 창을 확장하거나 탐색 모음 아이콘을 선택하여 사용자 이름 및 기타 링크를 표시해야 할 수 있습니다.
- 개인 데이터 탭을 선택합니다.
- 다운로드 단추를 선택하고 파일을 검사했습니다
PersonalData.json
. - 로그온한 사용자를 삭제하는 삭제 단추를 테스트합니다.
사용자 지정 사용자 데이터를 Identity DB에 추가
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; }
}
}
PersonalData 특성이 있는 속성은 다음과 같습니다.
Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml
Razor페이지에서UserManager.Delete
호출할 때 삭제됩니다.- 다운로드한 데이터에는
Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml
Razor 페이지가 포함됩니다.
Account/Manage/Index.cshtml 페이지 업데이트
다음 강조 표시된 코드로 Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
에서 InputModel
을 업데이트합니다.
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" />
}
Account/Register.cshtml 페이지 업데이트
다음 강조 표시된 코드로 Areas/Identity/Pages/Account/Register.cshtml.cs
에서 InputModel
을 업데이트합니다.
[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" />
}
프로젝트를 빌드합니다.
사용자 지정 사용자 데이터에 대한 마이그레이션 추가
Visual Studio 패키지 관리자 콘솔에서 다음을 실행합니다.
Add-Migration CustomUserData
Update-Database
사용자 지정 사용자 데이터 만들기, 보기, 다운로드, 삭제 테스트
앱을 테스트합니다.
- 새 사용자를 등록합니다.
/Identity/Account/Manage
페이지에서 사용자 지정 사용자 데이터를 봅니다./Identity/Account/Manage/PersonalData
페이지에서 사용자 개인 데이터를 다운로드하고 봅니다.
IUserClaimsPrincipalFactory<ApplicationUser>
를 사용하여 Identity에 클레임 추가
참고 항목
이 섹션은 이전 자습서의 확장이 아닙니다. 자습서를 사용하여 빌드된 앱에 다음 단계를 적용하려면 이 GitHub 이슈를 참조하세요.
IUserClaimsPrincipalFactory<T>
인터페이스를 사용하여 Identity에 추가 클레임을 추가할 수 있습니다. 이 클래스를 Startup.ConfigureServices
메서드의 앱에 추가할 수 있습니다. 다음과 같이 클래스의 사용자 지정 구현을 추가합니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>,
AdditionalUserClaimsPrincipalFactory>();
데모 코드는 ApplicationUser
클래스를 사용합니다. 이 클래스는 추가 클레임을 추가하는 데 사용되는 IsAdmin
속성을 추가합니다.
public class ApplicationUser : IdentityUser
{
public bool IsAdmin { get; set; }
}
인터페이스 AdditionalUserClaimsPrincipalFactory
를 UserClaimsPrincipalFactory
구현합니다. ClaimsPrincipal
에 새 역할 클레임이 추가됩니다.
public class AdditionalUserClaimsPrincipalFactory
: UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public AdditionalUserClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{}
public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
var identity = (ClaimsIdentity)principal.Identity;
var claims = new List<Claim>();
if (user.IsAdmin)
{
claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
}
else
{
claims.Add(new Claim(JwtClaimTypes.Role, "user"));
}
identity.AddClaims(claims);
return principal;
}
}
그런 다음, 앱에서 추가 클레임을 사용할 수 있습니다. Razor Page에서 IAuthorizationService
인스턴스를 사용하여 클레임 값에 액세스할 수 있습니다.
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
@if ((await AuthorizationService.AuthorizeAsync(User, "IsAdmin")).Succeeded)
{
<ul class="mr-auto navbar-nav">
<li class="nav-item">
<a class="nav-link" asp-controller="Admin" asp-action="Index">ADMIN</a>
</li>
</ul>
}
Razor 웹앱 만들기
- Visual Studio 파일 메뉴에서 새로 만들기>프로젝트를 선택합니다. 다운로드 샘플 코드의 네임스페이스와 일치하려면 프로젝트 이름을 WebApp1로 지정합니다.
- ASP.NET Core 웹 애플리케이션>확인을 선택합니다.
- 드롭다운에서 ASP.NET Core 2.2 선택
- 웹 애플리케이션>확인을 선택합니다.
- 프로젝트를 빌드하고 실행합니다.
Identity 스캐폴더 실행
- 솔루션 탐색기에서 프로젝트 > >추가>새 스캐폴드 항목을 마우스 오른쪽 단추로 클릭합니다.
- 스캐폴드 추가 대화 상자의 왼쪽 창에서 Identity>추가를 선택합니다.
- 추가 Identity 대화 상자에서 다음 옵션을 선택합니다.
- 기존 레이아웃 파일
~/Pages/Shared/_Layout.cshtml
을 선택합니다. - 재정의할 다음 파일을 선택합니다.
- Account/Register
- Account/Manage/Index
- + 단추를 선택하여 새 데이터 컨텍스트 클래스를 만듭니다. 형식(프로젝트 이름이 WebApp1인 경우 WebApp1.Models.WebApp1Context)을 적용합니다.
- + 단추를 선택하여 새 사용자 클래스를 만듭니다. 형식(프로젝트 이름이 WebApp1인 경우 WebApp1User) >추가를 적용합니다.
- 기존 레이아웃 파일
- 추가를 선택합니다.
마이그레이션, UseAuthentication 및 레이아웃의 지침에 따라 다음 단계를 수행합니다.
- 마이그레이션을 만들고 데이터베이스를 업데이트합니다.
UseAuthentication
를Startup.Configure
에 추가합니다.- 레이아웃 파일에
<partial name="_LoginPartial" />
을 추가합니다. - 앱을 테스트합니다.
- 사용자 등록
- 새 사용자 이름(로그아웃 링크 옆)을 선택합니다. 창을 확장하거나 탐색 모음 아이콘을 선택하여 사용자 이름 및 기타 링크를 표시해야 할 수 있습니다.
- 개인 데이터 탭을 선택합니다.
- 다운로드 단추를 선택하고 파일을 검사했습니다
PersonalData.json
. - 로그온한 사용자를 삭제하는 삭제 단추를 테스트합니다.
사용자 지정 사용자 데이터를 Identity DB에 추가
IdentityUser
파생 클래스를 사용자 지정 속성으로 업데이트합니다. WebApp1 프로젝트의 이름을 지정한 경우 파일 이름은 Areas/Identity/Data/WebApp1User.cs
입니다. 파일을 다음 코드로 업데이트합니다.
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 페이지 업데이트
다음 강조 표시된 코드로 Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
에서 InputModel
을 업데이트합니다.
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 페이지 업데이트
다음 강조 표시된 코드로 Areas/Identity/Pages/Account/Register.cshtml.cs
에서 InputModel
을 업데이트합니다.
[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 패키지 관리자 콘솔에서 다음을 실행합니다.
Add-Migration CustomUserData
Update-Database
사용자 지정 사용자 데이터 만들기, 보기, 다운로드, 삭제 테스트
앱을 테스트합니다.
- 새 사용자를 등록합니다.
/Identity/Account/Manage
페이지에서 사용자 지정 사용자 데이터를 봅니다./Identity/Account/Manage/PersonalData
페이지에서 사용자 개인 데이터를 다운로드하고 봅니다.
ASP.NET Core