WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Identity のユーザー管理 (MVC)

by WebSurfer 2017年10月29日 18:10

ASP.NET Identity ベースのフォーム認証を実装した ASP.NET MVC5 アプリケーションで、管理者がユーザー情報の追加、変更、削除を行う機能を実装する例を書きます。

ユーザー管理画面

CodeZine の記事「ASP.NET Identity でユーザーを管理する」に、ASP.NET Web Forms アプリ用のユーザー管理画面を作る手順が載っていますが、その MVC 版です。

ベースとなるのは、Visual Studio 2015 Community で MVC のテンプレートを利用し、認証に「個別のユーザーアカウント」を指定して自動生成させたプロジェクトです。設定は以下の画像を見てください。

MVC のテンプレート

上の設定で自動生成されたプロジェクトは ASP.NET Identity ベースのフォーム認証を使用する ASP.NET MVC5 アプリケーションの基本的機能を持ちます。

プロジェクトの作成後、Visual Studio で[デバッグの開始(S)]または[デバッグなしで開始(H)]で実行すると、Web サーバーとして IIS Express が立ち上がって web アプリが実行され、テンプレートに含まれている Home/Index がブラウザ(デフォルトで IE)に表示されます。

そこから[Register]をクリックしてユーザー登録を行うと、初回に EF Code First と LocalDB の機能を利用してユーザー情報のストア(.mdf ファイル)が App_Data フォルダに生成され、その後は登録した Email とパスワードでログイン可能になります。

その状態から、Web アプリケーションに管理者がユーザー情報の追加、変更、削除を行う機能を実装します。

ユーザー情報の追加、変更、削除を行うのに用いるクラスは以下の通りです。いずれもテンプレートから自動生成されたコードに完全な形で含まれているので、それらをそのまま利用します。

(1) Models/IdentityModels.cs

(2) App_Start/IdentityConfig.cs

また、App_Start/Startup.Auth.cs の Startup クラスで、ApplicationUserManager のインスタンスを OwinContext へ登録するコードも自動生成されます。

なので、Controller では HttpContext.GetOwinContext() で OwinContext を取得し、それから GetUserManager<ApplicationUserManager>() メソッドで ApplicationUserManager オブジェクトを取得できるようになっています。

それらを利用してユーザー情報の追加、変更、削除を行う Controller の実装例を以下に示します。説明はコード内のコメントに書きましたので、それを見てください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

// 上は自動生成されたもの。それに以下の名前空間を追加
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Mcv5App2.Models;
using System.Threading.Tasks;
using System.Net;
using System.ComponentModel.DataAnnotations;

namespace Mcv5App2.Controllers
{
  // Edit 用に AccountViewModels.cs の既存の RegisterViewModel
  // を流用しようとしたが、パスワードを入力しない場合は検証に
  // 引っかかっるので、以下の EditViewModel を定義して利用。
  // (Controller 内に定義したのは単に分けるのが面倒だから)
  public class EditViewModel
  {
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    // PasswordValidator を使えばここでの検証は不要
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [System.ComponentModel.DataAnnotations.Compare(
     "Password", 
     ErrorMessage = 
     "Password と Confirm Password が一致しません")]
    public string ConfirmPassword { get; set; }
  }


  // ここから Controller のコード
  public class UsersController : Controller
  {
    private ApplicationUserManager _userManager;

    public ApplicationUserManager UserManager
    {
      get
      {
        return _userManager ?? 
            HttpContext.GetOwinContext().
            GetUserManager<ApplicationUserManager>();
      }
      private set
      {
        _userManager = value;
      }
    }

    // GET: Users(登録済みユーザーの一覧)
    // Model は ApplicationUser
    public ActionResult Index()
    {
      var users = UserManager.Users.
                OrderBy(user => user.UserName);
      return View(users);
    }

    // GET: Users/Details/Id(指定 Id のユーザー詳細)
    // Model は ApplicationUser
    public async Task<ActionResult> Details(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      return View(target);
    }

    // GET: Users/Create(新規ユーザー作成・登録)
    // Model は AccountViewModels.cs の RegisterViewModel
    public ActionResult Create()
    {
      return View();
    }

    // POST: Users/Create(新規ユーザー作成・登録)
    // Model は AccountViewModels.cs の RegisterViewModel
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        Create(RegisterViewModel model)
    {
      // PasswordValidator による判定の結果は ModelState
      // には反映されないので注意(下記 result で判定)
      if (ModelState.IsValid)
      {
        // ユーザー入力のメールアドレスを UserName, Email
        // プロパティに設定して ApplicationUser を生成
        var user = new ApplicationUser {
                            UserName = model.Email,
                            Email = model.Email };

        // 上の ApplicationUser とユーザー入力のパスワ
        // ードで新規ユーザーを作成・登録
        var result = await UserManager.
                     CreateAsync(user, model.Password);

        // ユーザー作成・登録の成否を result.Succeeded で
        // 判定。PasswordValidator の判定結果も result に
        // 反映される
        if (result.Succeeded)
        {
          // 登録に成功したら Users/Index にリダイレクト
          return RedirectToAction("Index", "Users");
        }
        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        // AccountController と同様に AddErrors メソッドを
        // 定義して利用(一番下に定義あり)
        AddErrors(result);
      }

      // ユーザー登録に失敗した場合、登録画面を再描画
      return View(model);
    }

    // GET: Users/Edit/Id(指定 Id のユーザー情報の更新)
    // ここで更新できるのは UserName, Email, パスワード
    // のみ。
    // Model は上に定義した EditViewModel
    public async Task<ActionResult> Edit(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      EditViewModel model = 
        new EditViewModel() { Email = target.Email };

      return View(model);
    }

    // POST: Users/Edit/Id(指定 Id のユーザー情報の更新)
    // ここで更新できるのは UserName, Email, パスワード
    // のみ。
    // UserName をソルトに使っていてパスワードだけもしくは
    // UserName だけを更新するのは NG かと思っていたが問題
    // なかった。(実際どのように対処しているかは不明)
    // Model は上に定義した EditViewModel
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                  Edit(string id, EditViewModel model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        var target = await UserManager.FindByIdAsync(id);
        target.Email = model.Email;
        target.UserName = model.Email;

        // ユーザーが新パスワードを入力した場合はパス
        // ワードも更新する
        if (!string.IsNullOrEmpty(model.Password))
        {
          // PasswordValidator による検証
          var resultPassword = 
              await UserManager.PasswordValidator.
                    ValidateAsync(model.Password);

          if (resultPassword.Succeeded)
          {
            // 検証 OK の場合、入力パスワードを hash
            var hashedPassword = 
                UserManager.PasswordHasher.
                HashPassword(model.Password);
            target.PasswordHash = hashedPassword;
          }
          else
          {
            // 検証 NG の場合 ModelSate にエラー情報を
            // 追加して編集画面を再描画
            AddErrors(resultPassword);
            return View(model);
          }
        }

        var resultUpdate = 
          await UserManager.UpdateAsync(target);

        if (resultUpdate.Succeeded)
        {
          // 更新に成功したら Users/Index にリダイレクト
          return RedirectToAction("Index", "Users");
        }
        AddErrors(resultUpdate);
      }

      // 更新に失敗した場合、編集画面を再描画
      return View(model);
    }

    // GET: Users/Delete/Id(指定 Id のユーザーを削除)
    // 階層更新が行われているようで、ロールがアサインされて
    // いるユーザーも削除可能。
    // Model は ApplicationUser
    public async Task<ActionResult> Delete(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      return View(target);
    }

    // POST: Users/Delete/Id(指定 Id のユーザーを削除)
    // Model は ApplicationUser
    // 上の Delete(string id) と同シグネチャのメソッド
    // は定義できないので、メソッド名を変えて、下のよう
    // に ActionName("Delete") を設定する
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        DeleteConfirmed(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      // ロールがアサインされているユーザーも以下の一行で
      // 削除可能。内部で階層更新が行われているらしい。
      var result = await UserManager.DeleteAsync(target);

      if (result.Succeeded)
      {
        // 削除に成功したら Users/Index にリダイレクト
        return RedirectToAction("Index", "Users");
      }
      AddErrors(result);

      return View(target);
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
      {
        if (_userManager != null)
        {
          _userManager.Dispose();
          _userManager = null;
        }
      }

      base.Dispose(disposing);
    }

    // ModelSate にエラー情報を追加するためのヘルパメソッド
    private void AddErrors(IdentityResult result)
    {
      foreach (var error in result.Errors)
      {
        ModelState.AddModelError("", error);
      }
    }
  }
}

上のコードは MSDN ライブラリに書いてある非同期版のメソッドを使っていますが、同期版も拡張メソッドとして定義されているようです。ケースバイケースでどちらが適切かを考えて使い分けた方が良いかもしれません。

View はコントローラをベースにスキャフォールディング機能を利用して自動生成できます。Visual Studio でアクションメソッドのコードを右クリックして[ビューを追加(D)...]を選ぶと、以下の画像のダイアログが表示されます。

View の作成

ここで Template と Model class を設定して[Add]ボタンをクリックすれば View は自動生成されます。

Data context class は必ず空白にしてください。余計な設定をすると余計なコードが自動生成されて "Multiple object sets per type are not supported." というエラーになると思います。

Tags:

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2017年10月  >>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar