ASP.NET Core 프로젝트에서 Identity에 사용자 지정 사용자 데이터 추가, 다운로드 및 삭제

작성자: Rick Anderson

이 문서는 다음 방법을 안내합니다.

  • ASP.NET Core 웹앱에 사용자 지정 사용자 데이터를 추가합니다.
  • 사용자 지정 사용자 데이터 모델을 PersonalDataAttribute 특성으로 표시하여 다운로드 및 삭제에 자동으로 사용할 수 있도록 합니다. 데이터를 다운로드하고 삭제할 수 있도록 하면 GDPR 요구 사항을 충족하는 데 도움이 됩니다.

프로젝트 샘플은 Razor Pages 웹앱에서 생성되지만 지침은 ASP.NET Core MVC 웹앱에 대해 유사합니다.

샘플 코드 보기 및 다운로드(다운로드 방법)

필수 조건

.NET 6.0 SDK

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 및 레이아웃의 지침에 따라 다음 단계를 수행합니다.

  • 마이그레이션을 만들고 데이터베이스를 업데이트합니다.
  • 에 추가 UseAuthenticationProgram.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.cshtmlRazor페이지에서 UserManager.Delete 호출할 때 삭제됩니다.
  • 다운로드한 데이터에는 Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtmlRazor 페이지가 포함됩니다.

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">
                <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-floating">
                <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-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 페이지에서 사용자 개인 데이터를 다운로드하고 봅니다.

.NET Core 3.0 SDK

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 및 레이아웃의 지침에 따라 다음 단계를 수행합니다.

  • 마이그레이션을 만들고 데이터베이스를 업데이트합니다.
  • UseAuthenticationStartup.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.cshtmlRazor페이지에서 UserManager.Delete 호출할 때 삭제됩니다.
  • 다운로드한 데이터에는 Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtmlRazor 페이지가 포함됩니다.

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; }
}

인터페이스 AdditionalUserClaimsPrincipalFactoryUserClaimsPrincipalFactory 구현합니다. 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>
}

.NET Core 2.2 SDK 이상

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 및 레이아웃의 지침에 따라 다음 단계를 수행합니다.

  • 마이그레이션을 만들고 데이터베이스를 업데이트합니다.
  • UseAuthenticationStartup.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.cshtmlRazor페이지에서 UserManager.Delete 호출할 때 삭제됩니다.
  • 다운로드한 데이터에는 Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtmlRazor 페이지가 포함됩니다.

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 페이지에서 사용자 개인 데이터를 다운로드하고 봅니다.