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
모델을 사용하도록 수정해줘야 합니다. 수정할 파일은
- Startup.Auth.cs
- ApplicationOAuthProvider.cs
- AccountController.cs
입니다. 마지막 모델 작업은 입력 모델의 이메일 정보를 데이터 모델에 전달하는 것입니다. AccountController
의 Register
메서드에 RegisterBindingModel
을 User
로 변환하는 코드가 있습니다. 여기에 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>
결론
ASP.NET Identity 사용자 계정 모델을 확장하는 방법을 설명했습니다. 프로젝트 템플릿 코드에서부터 추가적인 멤버를 정의하지 않더라도 IdentityUser
클래스를 상속받은 클래스를 사용하도록 제공하면 귀찮은 작업들이 많이 줄었을 것이란 점이 아쉽습니다. 이미 한 번 언급했지만 하나의 개념적 모델을 다수의 클래스로 표현하는 방식 역시 관리에 어려움을 줍니다. 실제 서비스 구현 과정에서 사용자 모델의 확장은 빈번하게 벌어질 듯 한데요.