この記事は、以下2つの環境における ASP.NET Identity のカスタマイズ手順記録です。
- ASP.NET MVC 5 + ASP.NET Identity 2.2
- ASP.NET Core MVC 1.0 + ASP.NET Core Identity 1.0
Visual Studio Community 2015 で検証、Entity Framework を使用した Code First 開発を想定しています。
#ASP.NET Identityとは
ご存じとは思いますが念のため。
ASP.NET Identity はASP.NET標準搭載の認証・資格管理システムで、以下のような機能が簡単に実装できるようになります。
- ログイン認証
- ユーザー登録
- 仮登録メールの送信
- ユーザー権限の設定
- パスワードの再発行
- ソーシャルログイン認証
- アカウントロック
- 二要素認証の作成
これら全てを独自実装するのは大変なので、ASP.NETでログイン認証を持つWEBシステムを作るのであればほぼ必須の機能と言えます。
そのため基本的には ASP.NET Identity の仕様に合わせながら、必要な部分はカスタマイズで対応するのがよいと思います。
#ユーザーIdをint型にしたい
ASP.NET Identityで作成したユーザー情報をDB上で確認すると、主キーとなるIdは 文字列型 の GUID として登録されています。
ユーザーIdを対象とした総当たり攻撃を防ぐという意味では非常に効果的ではありますが、システム要件によってはユーザーIdは連番のほうが都合が良いこともあるかと思います。
そこで ASP.NET Identity をカスタマイズして、ユーザーIdを int型 に変更してみます。
##ASP.NET MVC 5 の場合
ASP.NET MVC 5 + ASP.NET Identity 2.2 で実装する場合です。
###ASP.NET MVC 5 プロジェクト作成
プロジェクトは、以下の手順で作成したものを使用します。
- 新しいプロジェクト > .NET Framework 4.6 > ASP.NET Web Application (.NET Framework)
- ASP.NET 4.6 テンプレート> MVC (認証:個別のユーザー アカウント)
###ソースコード確認
ApplicationUserクラスを確認すると、IdentityUserを継承しているようです。
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
IdentityUserの定義を確認してみると、IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim> を継承していました。
public class IdentityUser : IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUser, IUser<string>
{
public IdentityUser();
public IdentityUser(string userName);
}
IdentityUserLogin, IdentityUserRole, IdentityUserClaimも確認してみます。
public class IdentityUserLogin : IdentityUserLogin<string>
{
public IdentityUserLogin();
}
public class IdentityUserRole : IdentityUserRole<string>
{
public IdentityUserRole();
}
public class IdentityUserClaim : IdentityUserClaim<string>
{
public IdentityUserClaim();
}
それぞれ、型引数がstring型のジェネリッククラスを継承していることが確認できました。
ユーザーIdをint型にするためには、これらをすべてstring型からint型に変える必要があります。
###コード修正
型引数がint型のジェネリッククラスに変更したカスタムクラスを作成します。
public class CustomUserLogin : IdentityUserLogin<int> { }
public class CustomUserRole : IdentityUserRole<int> { }
public class CustomUserClaim : IdentityUserClaim<int> { }
public class CustomRole : IdentityRole<int, CustomUserRole> { }
このカスタムクラスを使って、関連クラスを置き換えていきます。
// IdentityUser -> IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
// UserManager<ApplicationUser> -> UserManager<ApplicationUser, int>
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, int> manager)
{
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
// IdentityDbContext<ApplicationUser> -> IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
// base("DefaultConnection", throwIfV1Schema: false) -> base("DefaultConnection")
public ApplicationDbContext()
: base("DefaultConnection")
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
// UserManager<ApplicationUser> -> UserManager<ApplicationUser, int>
public class ApplicationUserManager : UserManager<ApplicationUser, int>
{
// IUserStore<ApplicationUser> -> IUserStore<ApplicationUser, int>
public ApplicationUserManager(IUserStore<ApplicationUser, int> store)
: base(store)
{
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
// UserStore<ApplicationUser>() -> UserStore<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>()
var manager = new ApplicationUserManager(new UserStore<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>(context.Get<ApplicationDbContext>()));
...
public void ConfigureAuth(IAppBuilder app)
{
...
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// OnValidateIdentity<ApplicationUserManager, ApplicationUser> -> OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>(
validateInterval: TimeSpan.FromMinutes(30),
// regenerateIdentity -> regenerateIdentityCallback
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
// 【getUserIdCallbackを追加】
getUserIdCallback: (id) => id.GetUserId<int>())
}
});
あとは、あちこちでコンパイルエラーとなっている箇所に対して、下に挙げる変更を反映させていきます(20箇所くらい)。
// XxxxClass<ApplicationUser>
XxxxClass<ApplicationUser, int>
// User.Identity.GetUserId();
User.Identity.GetUserId<int>();
// function(string userId)
// if (userId == null)
function(int userId)
if (userId == default(int))
コンパイルエラーが無くなったらMigrationとUpdateを実行。
PM> Add-Migration Initial
PM> Update-Database
AspNetUsersテーブルのIdカラムが int型 になりました。
##ASP.NET Core MVC の場合
ASP.NET Core MVC + ASP.NET Core Identity 1.0 で実装する場合です。
###ASP.NET Core MVC プロジェクト作成
プロジェクトは、以下の手順で作成したものを使用します。
- 新しいプロジェクト > ASP.NET Core Web Application (.NET Core)
- ASP.NET Core Templates > Webアプリケーション (認証:個別のユーザー アカウント)
###ソースコード確認
ApplicationUserクラスを確認すると、同じくIdentityUserを継承しています。
public class ApplicationUser : IdentityUser
{
}
IdentityUserの定義を確認してみると、先ほどと違いIdentityUser<string>のみを継承しています。
更に、IdentityUser<TKey>の定義を確認すると、ASP.NET MVC 5 のときはバラバラに指定していた型引数が共通化されていました。
public class IdentityUser : IdentityUser<string>
{
public IdentityUser();
public IdentityUser(string userName);
}
public class IdentityUser<TKey> : IdentityUser<TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>, IdentityUserLogin<TKey>> where TKey : IEquatable<TKey>
{
public IdentityUser();
}
###コード修正
先ほどと同様にint型のジェネリッククラスに変更していきますが、型引数が共通化されたため、変更箇所は以下の3箇所だけで済みます。
// IdentityUser -> IdentityUser<int>
public class ApplicationUser : IdentityUser<int>
{
}
// IdentityDbContext<ApplicationUser> -> IdentityDbContext<ApplicationUser, IdentityRole<int>, int>
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<int>, int>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
...
}
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// AddIdentity<ApplicationUser, IdentityRole> -> AddIdentity<ApplicationUser, IdentityRole<int>>
// AddEntityFrameworkStores<ApplicationDbContext> -> AddEntityFrameworkStores<ApplicationDbContext, int>
services.AddIdentity<ApplicationUser, IdentityRole<int>>()
.AddEntityFrameworkStores<ApplicationDbContext, int>()
.AddDefaultTokenProviders();
...
初期配置されている Data\Migrations\00000000000000_CreateIdentitySchema.cs
と ApplicationDbContextModelSnapshot.cs
を削除してMigrationとUpdateを実行。
PM> Add-Migration Initial
PM> Update-Database
こちらも、AspNetUsersテーブルのIdカラムを int型 にすることができました。
実際にWeb画面上からユーザー登録してみると、Idが連番で登録されることが確認できます。
#最後に
ASP.NET Coreはクロスプラットフォームになっただけでなくコードも改良されているようなので、.NET Frameworkを使い続ける場合でも移行するメリットはあるかもしれません。