ハロの外部記憶インターフェイス

そろそろ覚える努力が必要かも…

ASP.Net MVC Identity 認証

ASP.NET MVC プロジェクトを作成

認証を選択するとデフォルトの認証ロジックが貼るため、認証なしで作成する。

プロジェクトにNugetから追加する。

  • Microsoft.Owin.Host.SystemWeb
    • OwinをIISで有効にするためのライブラリ
  • Microsoft.AspNet.Identity.Owin
    • 認証に必要なライブラリ

AppUser

UserNameとPasswordのみの簡単なものにします。 適当なフォルダーに作成します。

public class AppUser : IUser<string>
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    public string UserName { get; set; }

    public string Password { get; set; }
}

UserManager

UserStoreクラスを制御するラッピングクラス

Create(IdentityFactoryOptions, IOwinContext)を実装して、インスタンス生成時に呼ばれる様にしている。 現状としてはどんな仕組みで呼び出し時にCreateでパラメータを渡さずに動いているのかがさっぱりわからない。

using Microsoft.AspNet.Identity;

public class AppUserManager : UserManager<AppUser>
{
    private AppUserManager(IUserStore<AppUser> store)
        : base(store)
    { }

    public static AppUserMananger Create(IdentityFactoryOptions<AppUserMananger> options, IOwinContext context)
    {
        return new AppUserMananger(new AppUserStore());
    }
}

IRole

public class AppRole : IRole<string>
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    public string Name { get; set ; }
}

RoleMananager

using Microsoft.AspNet.Identity;

public class AppRoleManager : RoleManager<AppRole>
{
    public AppRoleManager(IRoleStore<AppRole, string> store) 
        : base(store)
    { }
}

SignInManager

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;

public class AppSignInManager : SignInManager<AppUser, string>
{
    private AppSignInManager(UserManager<AppUser, string> userManager, IAuthenticationManager authenticationManager)
        : base(userManager, authenticationManager)
    { }
    public static AppSignInManager Create(IdentityFactoryOptions<AppSignInManager> options, IOwinContext context)
    {
        AppUserMananger manager = context.GetUserManager<AppUserMananger>();
       return new AppSignInManager(manager, context.Authentication);
    }
}

UserStore

ユーザー情報を扱うためのクラス(CRUD処理)

public class AppUserStore : 
    IUserStore<AppUser>, 
    IUserStore<AppUser,string>,
    IUserPasswordStore<AppUser,string>,
    IUserRoleStore<AppUser, string>,
    IRoleStore<AppRole, string>
{
    // DBの代わりにダミーデータ設定
    
    // ユーザマスタ
    private static List<AppUser> Users { get; } = new List<AppUser>
    {
        new AppUser { Id = "1", UserName = "user1", Password="abc" },
        new AppUser { Id = "2", UserName = "user2", Password="def" }
    };

    // ロールマスタ
    private static List<AppRole> Roles { get; } = new List<AppRole>
    {
        new AppRole { Id = "1", Name = "emploryee"},
        new AppRole { Id = "999", Name = "admin" }
    };

    // ユーザー・ロールマスタ
    private static List<Tuple<string, string>> UserRoleMap { get; } = new List<Tuple<string, string>>
    {
        Tuple.Create("1", "1"),
        Tuple.Create("2","999")
    };

    public Task AddToRoleAsync(AppUser user, string roleName)
    {
        var role = Roles.FirstOrDefault(x => x.Name == roleName);
        if (role == null) { throw new InvalidOperationException(); }

        var userRoleMap = UserRoleMap.FirstOrDefault(x => x.Item1 == user.Id && x.Item2 == role.Id);
        if (userRoleMap == null)
        {
            UserRoleMap.Add(Tuple.Create(user.Id, role.Id));
        }

        return Task.Delay(0);
    }

    public Task CreateAsync(AppUser user)
    {
        Users.Add(user);
        return Task.Delay(0);
    }

    public Task CreateAsync(AppRole role)
    {
        Roles.Add(role);
        return Task.Delay(0);
    }

    public Task DeleteAsync(AppUser user)
    {
        Users.Remove(Users.First(x => x.Id == user.Id));
        return Task.Delay(0);
    }

    public Task DeleteAsync(AppRole role)
    {
        Roles.Remove(Roles.First(x => x.Id == role.Id));
        return Task.Delay(0);
    }

    public void Dispose()
    {
    }

    public Task<AppUser> FindByIdAsync(string userId)
    {
        return Task.FromResult(Users.FirstOrDefault(u => u.Id == userId));
    }

    public Task<AppUser> FindByNameAsync(string userName)
    {
        return Task.FromResult(Users.FirstOrDefault(u => u.UserName == userName));
    }

    public Task<string> GetPasswordHashAsync(AppUser user)
    {
        return Task.FromResult(new PasswordHasher().HashPassword(user.Password));
    }

    public Task<IList<string>> GetRolesAsync(AppUser user)
    {
        IList<string> roleNames = UserRoleMap.Where(x => x.Item1 == user.Id)
            .Select(x => x.Item2)
            .Select(x => Roles.First(y => y.Id == x))
            .Select(x => x.Name)
            .ToList();
        return Task.FromResult(roleNames);
    }

    public Task<bool> HasPasswordAsync(AppUser user)
    {
        return Task.FromResult(user.Password != null);
    }

    public async Task<bool> IsInRoleAsync(AppUser user, string roleName)
    {
        var roles = await this.GetRolesAsync(user);
        return roles.FirstOrDefault(x => x.ToUpper() == roleName.ToUpper()) != null;
    }

    public Task RemoveFromRoleAsync(AppUser user, string roleName)
    {
        var role = Roles.FirstOrDefault(x => x.Name == roleName);
        if (role == null) { return Task.FromResult(default(object)); }
        var userRoleMap = UserRoleMap.FirstOrDefault(x => x.Item1 == user.Id && x.Item2 == role.Id);
        if (UserRoleMap != null)
        {
            UserRoleMap.Remove(userRoleMap);
        }
        return Task.Delay(0);
    }

    public Task SetPasswordHashAsync(AppUser user, string passwordHash)
    {
        user.Password = passwordHash;
        return Task.Delay(0);
    }

    public async Task UpdateAsync(AppUser user)
    {
        var target = await this.FindByIdAsync(user.Id);
        if (target == null) { return; }
        target.UserName = user.UserName;
        target.Password = user.Password;
        return;
    }

    public Task UpdateAsync(AppRole role)
    {
        var t = Roles.FirstOrDefault(r => r.Id == role.Id);
        if (t == null) { return Task.FromResult(default(object)); }
        t.Name = role.Name;
        return Task.FromResult(default(object));
    }

    Task<AppRole> IRoleStore<AppRole, string>.FindByIdAsync(string roleId)
    {
        return Task.FromResult(Roles.FirstOrDefault(u => u.Id == roleId));
    }

    Task<AppRole> IRoleStore<AppRole, string>.FindByNameAsync(string roleName)
    {
        return Task.FromResult(Roles.FirstOrDefault(u => u.Name == roleName));
    }
}

StartUp

ルートのStartUp.csを作成します。

using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Owin;

[assembly: OwinStartup(typeof(MvcWeb2.OwinStartUp))]
namespace MvcWeb2
{
    public class OwinStartUp
    {
        public void Configuration(IAppBuilder app)
        {
            // 後から「HttpContext.GetOwinContext().Get」で取り出して使える。
            // Visual Studioのテンプレートに書いているコメントですが、だそうです。(DBは実装していないので、DbContextは作成していません。

            app.CreatePerOwinContext<AppUserMananger>(AppUserMananger.Create);
            app.CreatePerOwinContext<AppSignInManager>(AppSignInManager.Create);
            app.CreatePerOwinContext<AppRoleManager>((options, context) 
                => new AppRoleManager(context.Get<AppUserStore>()));
            app.CreatePerOwinContext<AppSignInManager>((options, context) 
                => new AppSignInManager(context.GetUserManager<AppUserManager>(), context.Authentication));

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Auth/Login")
            });
        }
    }
}

Controller

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

[Authorize]
public class AuthController : Controller
{
    private AppSignInManager _appSignInManager;
    private AppUserManager _appUserManager;

    public AppSignInManager SignInManager
    {
        get
        {
            // StartUp.csでCreatePerOwinContext登録したオブジェクトを取り出す。
            return _appSignInManager ?? HttpContext.GetOwinContext().Get<AppSignInManager>();
        }
        private set
        {
            _appSignInManager = value;
        }
    }

    public AppUserManager UserManager
    {
        get
        {
            // StartUp.csでCreatePerOwinContext登録したオブジェクトを取り出す。
            return _appUserManager ?? HttpContext.GetOwinContext().Get<AppUserManager>();
        }
        private set
        {
            _appUserManager = value;
        }
    }

    // GET: Auth/Login
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View(new LoginViewModel());
    }

    //
    // POST: /Auth/Login
    [AllowAnonymous]
    [HttpPost]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!this.ModelState.IsValid)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View(model);
        }

        // 認証 (SignInManagerを使用しない場合)
        //var userManager = new UserManager<AppUser>(new AppUserStore());
        //var user = await userManager.FindAsync(model.UserName, model.Password);
        //if (user == null)
        //{
        //    // 認証失敗したらエラーメッセージを設定してログイン画面を表示する
        //    this.ModelState.AddModelError("", "ユーザ名かパスワードが違います");
        //    ViewBag.ReturnUrl = returnUrl;
        //    return View(model);
        //}

        //// クレームベースのIDを作って
        //var identify = await userManager.CreateIdentityAsync(
        //    user,
        //    DefaultAuthenticationTypes.ApplicationCookie);

        //// 認証情報を設定
        //var authentication = this.HttpContext.GetOwinContext().Authentication;
        //authentication.SignIn(identify);

        // SignInManagerを利用する場合
        var user = await UserManager.FindAsync(model.UserName, model.Password);
        if (user == null) {
            this.ModelState.AddModelError("", "ユーザ名かパスワードが違います");
            return View(model);
        }

        await SignInManager.SignInAsync(user, false, false);

        // 元のページへリダイレクト
        return Redirect(returnUrl);
    }
}

Auth/LoginView

@model MvcWeb2.Controllers.LoginViewModel

@{
    ViewBag.Title = "Login";
}

<h2>Login</h2>
@Html.ValidationSummary(true)

@* ReturnUrlをパラメータに渡すようにしたフォームを作る *@
@using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl }))
{
    <div class="form-group">
        @Html.LabelFor(m => m.UserName)
        @Html.TextBoxFor(m => m.UserName)
        @Html.ValidationMessageFor(m => m.UserName)
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password)
        @Html.PasswordFor(m => m.Password)
        @Html.ValidationMessageFor(m => m.Password)
    </div>

    <input type="submit" value="ログイン" class="btn-default" />
}

Auth/LoginViewModel

public class LoginViewModel
{
    [Required(ErrorMessage = "ユーザー名を入れてください")]
    [Display(Name = "ユーザー名")]
    public string UserName { get; set; }

    [Required(ErrorMessage = "パスワードを入れてください")]
    [Display(Name = "パスワード")]
    public string Password { get; set; }
}