ASP.NET Identity 사용자 모델 확장

5/15/2014 추가 Microsoft ASP.NET Identity EntityFramework 패키지가 2.0.1 버전으로 업데이트 되었습니다. IdentityUser 클래스에 이메일 주소가 포함되었고 UserValidator 클래스를 사용해 이메일 주소 중복 여부를 검사할 수 있습니다.

사용자 모델

Microsoft ASP.NET Identity EntityFramework 패키지는 응용프로그램 사용자를 나타내는 IdentityUser 모델 클래스를 제공합니다. 최근의 ASP.NET 프로젝트 템플릿은 이 클래스와 EntityFramework을 사용해 개별 계정 관리를 구현합니다. 다음은 IdentityUser 클래스 정의입니다.

using Microsoft.AspNet.Identity;
using System;
using System.Collections.Generic;

namespace Microsoft.AspNet.Identity.EntityFramework
{
    public class IdentityUser : IUser
    {
        public IdentityUser();
        public IdentityUser(string userName);

        public virtual ICollection<IdentityUserClaim> Claims { get; }
        public virtual string Id { get; set; }
        public virtual ICollection<IdentityUserLogin> Logins { get; }
        public virtual string PasswordHash { get; set; }
        public virtual ICollection<IdentityUserRole> Roles { get; }
        public virtual string SecurityStamp { get; set; }
        public virtual string UserName { get; set; }
    }
}

제가 아쉬운 것은 모델에 이메일 주소가 포함되어 있지 않다는 점입니다. 사용자 계정 정보에 이메일 주소 속성을 추가하려면 조금 귀찮은 작업을 해줘야합니다. 이 포스트에서는 SPA(Single-Page Application) 프로젝트를 기준으로 사용자 모델을 확장하는 방법을 설명합니다.

글을 작성하는 시점에서는 Knockout을 사용한 ASP.NET SPA 템플릿만 제공됩니다. Angular 기반 템플릿도 곧 추가되겠죠?

5/12/2014 추가 – AngularJS SPA Template이 있었던 것을 몰랐네요. 확장 관리자에서 설치할 수 있습니다.

모델 확장

사용자 모델에 새로운 속성을 추가하려면 우선 IdentityUser 클래스를 상속받는 새로운 모델을 정의해야 합니다. 이름은 User라고 하고 Email 문자열 속성을 정의합니다. Email 속성에는 다음 4개의 특성을 추가하겠습니다.

특성 설명
Required Email 속성을 필수 속성으로 지정합니다.
EmailAddress Email 속성 값의 유효성을 검사합니다. 소스 코드를 참고하세요.
StringLength 문자열 길이를 지정하지 않으면 데이터 형식이 nvarchar(MAX)가 됩니다. Email 속성이 서로 중복되지 않도록 인덱스를 적용하려면 문자열 길이를 지정해줘야 합니다.
Index EntityFramework 6.1에 추가된 특성입니다. HOORAY! Email 속성에 인덱스를 적용합니다. EntityFramework 패키지 버전이 6.1 미만이라면 업데이트트 해줘야합니다.

PM> Update-Package EntityFramework

Display 특성은 지정하지 않았습니다. 지역화 리소스를 사용할 것이 아니라면 저는 굳이 ‘E’와 ‘m’ 사이에 하이픈을 넣고싶지 않아요. 🙂

public class User : IdentityUser
{
    [Required]
    [EmailAddress]
    [StringLength(128)]
    [Index(IsUnique = true)]
    public string Email { get; set; }
}

5/12/2014 추가 – Roles 속성을 sealed로 재정의하면 사용자 역할 모델이 정상적으로 동작하지 않습니다. UserStore<TUser> 클래스의 IsInRoleAsync(TUser, string) 메서드가 Roles 속성의 지연된 로딩(lazy loading)을 사용하기 때문입니다. ‘비동기인듯 비동기아닌 비동기같은 너.’ 연관 엔터티 로딩은 여기를 참조하세요.

public virtual Task<bool> IsInRoleAsync(TUser user, string role)
{
    this.ThrowIfDisposed();
    if (user == null)
    {
        throw new ArgumentNullException("user");
    }
    if (string.IsNullOrWhiteSpace(role))
    {
        throw new ArgumentException(IdentityResources.ValueCannotBeNullOrEmpty, "role");
    }
    return Task.FromResult<bool>(user.Roles.Any<IdentityUserRole>((IdentityUserRole r) => r.Role.Name.ToUpper() == role.ToUpper()));
}

저는 이 사실을 모르고 User 엔터티를 외부에 노출하기 위해 숨겨야할 속성들을 재정의 할 때 Roles 속성을 sealed로 만들었다가 한참 후에 역할 확인이 안되는 현상을 발견하고 이유를 몰라 고생했습니다.

[JsonIgnore]
public sealed override ICollection<IdentityUserRole> Roles
{
    get { return base.Roles; }
}

5/12/2014 추가 – 대부분의 경우 사용자 모델과 관련된 여러가지 모델을 정의하게 됩니다. Entity Framework를 사용할 경우 데이터베이스 컨텍스트는 IdentityDbContext<TUser>를 상속받으면 Code First 마이그레이션 등을 적용할 때 문제가 발생하지 않습니다.

컨트롤러에서 사용하는 바인딩 모델에도 Email 속성을 추가해줘야 합니다. 그래야 API 요청을 통해 이메일을 입력받을 수 있습니다. 귀찮습니다. 하나의 모델 클래스로 데이터 모델(Entity Model), 입력 모델(Binding Model), 출력 모델(View Model, MVVM 디자인 패턴의 ViewModel과 다릅니다.) 모두를 정의할 수 있는 도구가 등장하기를 기대해보지만 우선 지금은 각각 처리해 줄 수 밖에 없습니다.

public class RegisterBindingModel
{
    [Required]
    [Display(Name = "User name")]
    public string UserName { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} 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; }

    [Required]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

다음에는 기존의 IdentityUser 모델을 사용하던 코드를 User 모델을 사용하도록 수정해줘야 합니다. 수정할 파일은

  1. Startup.Auth.cs
  2. ApplicationOAuthProvider.cs
  3. AccountController.cs

입니다. 마지막 모델 작업은 입력 모델의 이메일 정보를 데이터 모델에 전달하는 것입니다. AccountControllerRegister 메서드에 RegisterBindingModelUser로 변환하는 코드가 있습니다. 여기에 Email 속성 처리를 추가합니다.

User user = new User
{
    UserName = model.UserName,
    Email = model.Email
};

뷰모델(register.viewmodel.js)

서버측 작업은 완료되었으니 이제 클라이언트 코드를 수정합니다. 우선 RegisterViewModel(네, 이번에는 MVVM 디자인 패턴의 ViewModel입니다.)이 이메일 데이터를 처리하도록 하겠습니다. 이름이 email인 속성을 추가하고 사용자를 등록할 때 이 속성값을 전달하도록 합니다.

// Data
self.userName = ko.observable("").extend({ required: true });
self.email = ko.observable("").extend({ required: true });
self.password = ko.observable("").extend({ required: true });
self.confirmPassword = ko.observable("").extend({ required: true, equal: self.password });
dataModel.register({
  userName: self.userName(),
  email: self.email(),
  password: self.password(),
  confirmPassword: self.confirmPassword()
}).done(function (data) {

뷰(_Register.cshtml)

마지막으로 뷰를 처리합니다. 이메일을 입력하는 요소를 배치하고 뷰모델 email 속성에 바인딩하면 마무리됩니다.

<div class="form-group">
  <label for="RegisterUserName" class="col-md-2 control-label">User name</label>
  <div class="col-md-10">
    <input type="text" id="RegisterUserName" class="form-control" data-bind="value: userName, hasFocus: true" />
  </div>
</div>
<div class="form-group">
  <label for="RegisterEmail" class="col-md-2 control-label">Email</label>
  <div class="col-md-10">
    <input type="text" id="RegisterEmail" class="form-control" data-bind="value: email" />
  </div>
</div>
<div class="form-group">
  <label for="RegisterPassword" class="col-md-2 control-label">Password</label>
  <div class="col-md-10">
    <input type="password" id="RegisterPassword" class="form-control" data-bind="value: password" />
  </div>
</div>
<div class="form-group">
  <label for="RegisterConfirmPassword" class="col-md-2 control-label">Confirm password</label>
  <div class="col-md-10">
    <input type="password" id="RegisterConfirmPassword" class="form-control" data-bind="value: confirmPassword" />
  </div>
</div>

Screen Shot 2014-04-29 at 2.32.21 PM

결론

ASP.NET Identity 사용자 계정 모델을 확장하는 방법을 설명했습니다. 프로젝트 템플릿 코드에서부터 추가적인 멤버를 정의하지 않더라도 IdentityUser 클래스를 상속받은 클래스를 사용하도록 제공하면 귀찮은 작업들이 많이 줄었을 것이란 점이 아쉽습니다. 이미 한 번 언급했지만 하나의 개념적 모델을 다수의 클래스로 표현하는 방식 역시 관리에 어려움을 줍니다. 실제 서비스 구현 과정에서 사용자 모델의 확장은 빈번하게 벌어질 듯 한데요.

Advertisements

ASP.NET Identity 사용자 모델 확장”에 대한 2개의 생각

  1. Jihyung Ryu

    글 잘 읽었습니다. 마침 같은 일을 하고 있어 몇자 적어봅니다.

    SPA 템플릿과 달리 MVC 템플릿에는 IdentityModel.cs에 ApplicationUser : IdentityUser 가 기본으로 포함되어 있습니다. 필요한 사용자 프로필 속성은 ApplicationUser 클래스에 추가할 수 있어 조금 더 편하다고 할 수 있겠습니다.

    EF의 IdentityUser 클래스를 사용해도 되지만, Core에 정의된 Microsoft.AspNet.Identity.Core.IUser 를 직접 구현해도 됩니다. Microsoft.AspNet.Identity.EntityFramework.IdentityUser 는 EF가 기본으로 제공하는 IUser 구현입니다.

    응답
    1. Gyuwon 글의 글쓴이

      좋은 정보 감사합니다. 🙂 MVC 사용한지 오래되어서 SPA 템플릿과 Identity 코드가 다르다는 생각은 못했네요.
      IUser 인터페이스를 직접 구현하게되면 Microsoft.AspNet.Identity.EntityFramework을 사용하지 못하는 단점이 있습니다. 그러고 보니 포스트 제목이 이것과 관련되어 조금 혼동을 줄 수 있겠군요.

      응답

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중