WebMatrix.WebData.WebSecurity を利用したユーザー認証(その1)

実生活で接点のない人のユーザー認証を行うウェブサイトの構築を考えたときに、ASP.NET MVC で提供されているものよりも WebMatrix で提供されているもののほうが魅力的に見える点があります。

  • メールを利用した登録確認が行える
  • パスワード忘れによるパスワード・リセットの際に、リセットの手続きの前に本人確認をメールで行うことで、新しいパスワードの受け渡しの煩雑さを回避できる(この点については「秘密の質問」という手もありますけど)

そこで、「Razor 構文と ASP.NET Web ページ」の「第 16 章 セキュリティとメンバーシップの追加」を参考にして、認証に WebMatrix.WebData.WebSecurity を利用するものを書いてみました。なんとなく面白そうですしね 😉

(2013/06/25 追記)
ASP.NET MVC 4 で WebMatrix で提供されているユーザー認証を利用する記事を書きました。

その1では、利用者登録とログインができるところまで作ります。
動きはこんな感じです。

ログイン画面

利用者登録画面

利用者登録を行うと、確認用のメールが届きます。

届いたメールの内容

URL をクリックして確認用のページを表示して、確認キーを入力することで登録手続きが完了します。

登録の確認画面

前提事項として、SMTP 接続には SmtpOverSsl.dll を使っています。
プログラムに行く前に、参照設定が必要です。WebMatrix と SmtpOverSsl.dll への参照を設定します。

  • WebMatrix.Data
  • WebMatrix.WebData
  • SmtpOverSsl.dll

また、ユーザー情報を登録するデータベースを用意する必要があります。WebMatrix で「スターター サイト」テンプレートから Web サイトを作成して、「App_Data」フォルダに生成される「StarterSite.sdf」をプロジェクトの「App_Data」フォルダへコピーします(ファイル名による DB 接続なので、ファイルが存在しないと「接続文字列 “StarterSite” が見つかりませんでした。」という例外が発生する)。なお、一番最初にログインするユーザー(ロールが存在しない状態でのログイン成功時)は、「admin」ロールを作成したうえで、admin ロールに所属させるようにしています(なので、「Razor 構文と ASP.NET Web ページ」の例にあるような手動での DB 操作は必要ありません。)。

プログラムですが、まずは Web.config にWebMatrix を利用するときのログインページの指定を設定する必要があります(デフォルトの設定が ASP.NET MVC が生成するものと違うんですよねぇ。。。)。

<?xml version="1.0" encoding="utf-8"?>
~略~
  <appSettings>
    <!-- WebMatrix を利用するときのログインページ指定に必要 -->
    <add key="loginUrl" value="~/Account/LogOn"/>
~略~
  </appSettings>
~略~
</configuration>

次にデータベース接続の設定をするために、プロジェクト直下の「Global.asax.cs」を修正します。
ここで SMTP 接続の設定も行います。(2013/06/17 修正。WebMatrix 側の _AppStart.cshtml の記述を掲載しちゃってたので修正しました :mrgreen: )

using System.Web.Mvc;
using System.Web.Routing;
using WebMatrix.WebData;
using MakCraft.SmtpOverSsl;

namespace TestAuthorize002
{
~略~
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            // WebMatrix 系の初期化コンテンツ
            WebSecurity.InitializeDatabaseConnection("StarterSite", "UserProfile", "UserId", "Email", true);
            SmtpMail.ServerName = "利用する SMTP サーバーのホスト名";
            SmtpMail.ServerPort = 利用する SMTP サーバーのポート番号;
            SmtpMail.EnableSsl = SSL 接続を行うか?;
            SmtpMail.AuthUserName = "SMTP 認証で利用するユーザー名";
            SmtpMail.AuthPassword = "SMTP 認証で利用するパスワード";
            SmtpMail.AuthMethod = SMTP 認証の方式;
            SmtpMail.MailEncoding = "ISO-2022-JP";
        }
    }
}

次に、モデル AccountModels.cs です。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Web.Mvc;
using System.Web.Security;

namespace TestAuthorize002.Models
{
~略~
    public class RegisterModel
    {
        [Required]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "電子メール アドレス")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "パスワードの確認入力")]
        [Compare("Password", ErrorMessage = "パスワードと確認のパスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }

    public class ConfirmModel
    {
        [Required]
        [Display(Name = "認証キー")]
        public string confirmationCode { get; set; }
    }
}

次に、コントローラー AccountController.cs です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Security;
using TestAuthorize002.Models;
using WebMatrix.Data;
using WebMatrix.WebData;
using MakCraft.SmtpOverSsl;

namespace TestAuthorize002.Controllers
{
~略~
        //
        // POST: /Account/LogOn

        [HttpPost]
        public ActionResult LogOn(LogOnModel model, string returnUrl)
        {
            var limitNumberOfMistake = 4;
            var lockedTime = 60;

            if (ModelState.IsValid)
            {
                // (パスワードの誤入力が 4回を超えている and 前回の誤入力から 60秒以内) であれば、アカウント・ロックを表示する
                if (WebSecurity.UserExists(model.UserName) &&
                    WebSecurity.GetPasswordFailuresSinceLastSuccess(model.UserName) > limitNumberOfMistake &&
                    WebSecurity.GetLastPasswordFailureDate(model.UserName).AddSeconds(lockedTime) > DateTime.UtcNow)
                    return RedirectToAction("AccountLockedOut");

                if (WebSecurity.Login(model.UserName, model.Password, model.RememberMe))
                {
                    const string roleName = "admin";
                    // ログイン成功のときに、ロール「admin」が無ければ、ロールを作成して、ログインユーザーを所属させる。
                    if (!Roles.Provider.RoleExists(roleName))
                    {
                        try
                        {
                            Roles.Provider.CreateRole(roleName);
                            Roles.AddUserToRole(model.UserName, roleName);
                        }
                        catch (System.Configuration.Provider.ProviderException)
                        {
                            ModelState.AddModelError("", "データベース接続でエラーが発生しました。");
                            return View();
                        }
                    }

                    if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }

                ModelState.AddModelError("", "ユーザー名またはパスワードが間違っています。");
            }

            // ここで問題が発生した場合はフォームを再表示します
            return View(model);
        }

        //
        // GET: /Account/AccountLockedOut

        public ActionResult AccountLockedOut()
        {
            return View();
        }

        //
        // GET: /Account/LogOff

        public ActionResult LogOff()
        {
            WebSecurity.Logout();

            return RedirectToAction("Index", "Home");
        }

        //
        // GET: /Account/Register

        public ActionResult Register()
        {
            return View();
        }

        //
        // POST: /Account/Register

        [HttpPost]
        public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                var db = Database.Open("StarterSite");

                // Check if user already exists
                var user = db.QuerySingle("SELECT Email FROM UserProfile WHERE LOWER(Email) = LOWER(@0)", model.Email);
                //if (user == null)

                if (!WebSecurity.UserExists(model.Email))
                {
                    // Insert email into the profile table
                    db.Execute("INSERT INTO UserProfile (Email) VALUES (@0)", model.Email);

                    // Create and associate a new entry in the membership database.
                    // If successful, continue processing the request
                    try
                    {
                        var token = WebSecurity.CreateAccount(model.Email, model.Password, true);
                        var hostUrl = Request.Url.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
                        var confirmationUrl = hostUrl +
                            VirtualPathUtility.ToAbsolute("~/Account/Confirm?confirmationCode="
                            + HttpUtility.UrlEncode(token));
                        var mail = new SmtpMailMessage
                        {
                            From = new System.Net.Mail.MailAddress("送信元のメールアドレスを記入する"),
                            To = new System.Net.Mail.MailAddress(model.Email),
                            Subject = "ユーザー登録の確認",
                            Body = "ユーザー登録の確認キー: " + token + "\r\n" +
                                "確認を行うページ <" + confirmationUrl + "> " +
                                "を訪問して、上記の「ユーザー登録の確認キー」を入力することで、ユーザー登録の確認を行ってください。"
                        };
                        SmtpMail.Send(mail);

                        return RedirectToAction("Thanks");
                    }
                    catch (ApplicationException ex)
                    {
                        ModelState.AddModelError("", "メールが送れませんでした。エラー内容: " + ex.Message);
                        return View();
                    }
                    catch (System.Net.Sockets.SocketException ex)
                    {
                        ModelState.AddModelError("", "メール送信サーバーと接続ができませんでした。エラー内容: " + ex.Message);
                        return View();
                    }
                    catch (System.Security.Authentication.AuthenticationException ex)
                    {
                        ModelState.AddModelError("", "メールサーバーと SSL 接続ができませんでした。エラー内容: " + ex.Message);
                        return View();
                    }
                    catch (System.Web.Security.MembershipCreateUserException e)
                    {
                        ModelState.AddModelError("", e.ToString());
                        return View();
                    }
                }
                else
                {
                    // User already exists
                    ModelState.AddModelError("", "Email address is already in use.");
                    return View();
                }
            }

            // ここで問題が発生した場合はフォームを再表示します
            return View(model);
        }

~略~
        // GET: /Acount/Thanks

        public ActionResult Thanks()
        {
            return View();
        }

        // GET: /Acount/Confirm

        public ActionResult Confirm()
        {
            return View();
        }

        //
        // POST: /Account/Confirm

        [HttpPost]
        public ActionResult Confirm(ConfirmModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            WebSecurity.Logout();

            if (WebSecurity.ConfirmAccount(model.confirmationCode))
            {
                TempData["Message"] = "登録が完了しました。登録したメールアドレスとパスワードでログインしてください。";
            }
            else
            {
                ModelState.AddModelError("", "登録情報の確認ができませんでした。");
            }

            return View();
        }
~略~
}

View は5つ(作成するのは3つ)です。
LogOn.cshtml


@model TestAuthorize002.Models.LogOnModel

@{
    ViewBag.Title = "ログオン";
}

<h2>ログオン</h2>
<p>
    ユーザー名(メールアドレス)とパスワードを入力してください。 @Html.ActionLink("登録", "Register") アカウントを持っていなかったら登録してください。
</p>
<p>
    @Html.ActionLink("パスワードのリセット", "ForgotPassword") パスワードが分からなくなったらパスワードをリセットしてください。
</p>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@Html.ValidationSummary(true, "ログインに失敗しました。エラーを訂正してもう一度試してください。")

@using (Html.BeginForm()) {
    <div>
        <fieldset>
            <legend>Account Information</legend>

            <div class="editor-label">
                @Html.LabelFor(m => m.UserName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.UserName)
                @Html.ValidationMessageFor(m => m.UserName)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Password)
            </div>
            <div class="editor-field">
                @Html.PasswordFor(m => m.Password)
                @Html.ValidationMessageFor(m => m.Password)
            </div>

            <div class="editor-label">
                @Html.CheckBoxFor(m => m.RememberMe)
                @Html.LabelFor(m => m.RememberMe)
            </div>

            <p>
                <input type="submit" value="Log On" />
            </p>
        </fieldset>
    </div>
}

Register.cshtml

@model TestAuthorize002.Models.RegisterModel

@{
    ViewBag.Title = "登録";
}

<h2>アカウントの新規作成</h2>
<p>
    新しいアカウントを作成するには、以下のフォームを使用してください。
</p>
<p>
    パスワードは、@Membership.MinRequiredPasswordLength 文字以上であることが必要です。
</p>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Sign-up Form</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Email)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Email)
            @Html.ValidationMessageFor(model => model.Email)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Password)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Password)
            @Html.ValidationMessageFor(model => model.Password)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.ConfirmPassword)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ConfirmPassword)
            @Html.ValidationMessageFor(model => model.ConfirmPassword)
        </div>

        <p>
            <input type="submit" value="Register" title="Register" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

AccountLockedOut.cshtml (厳密に型指定されたビューを作成する のチェックなし)

@{
    ViewBag.Title = "AccountLockedOut";
}

<h2>AccountLockedOut</h2>

<div class="message error">
    あなたのアカウントは、規定の回数以上のパスワードの誤りのためロックされました。
</div>
<p>
    アカウントのロックは60秒後に自動的に解除されます。
    その時間が過ぎた後に、もう一度試してください。
</p>

Thanks.cshtml (厳密に型指定されたビューを作成する のチェックなし)

@{
    ViewBag.Title = "Thanks for registering";
}

    <h2>But you’re not done yet!</h2>
    <p>
       An email with instructions on how to activate your account is on its way
       to you.
    </p>

Confirm.cshtml (厳密に型指定されたビューを作成する のチェックあり:ConfirmModel (TestAuthorize002.Models), Create)

@model TestAuthorize002.Models.ConfirmModel

@{
    ViewBag.Title = "Registration Confirmation Page";
}

<h2>Confirm</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

<div style="color:Red; margin-bottom:2em; font-size:14pt;">@TempData["Message"]</div>

@{
    if (TempData["Message"] == null)
    {
        using (Html.BeginForm())
        {
            @Html.ValidationSummary(true)
            <fieldset>
             <legend>Confirmation Code</legend>

                <p>Please enter the confirmation code sent to you via email and then click the <em>Confirm</em> button.</p>

                <div class="editor-label">
                    @Html.LabelFor(model => model.confirmationCode)
                </div>
                <div class="editor-field">
                    @Html.EditorFor(model => model.confirmationCode)
                    @Html.ValidationMessageFor(model => model.confirmationCode)
                </div>

                <p>
                    <input type="submit" value="Confirm" title="Confirm registration" />
                </p>
            </fieldset>
        }
    }
}

<div>
    @Html.ActionLink("Back to Top-page", "Index")
</div>

ここまでで、利用者登録とログインができるようになります。

その2」へ続きます。


WebMatrix.WebData.WebSecurity を利用したユーザー認証(その1)」への2件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です