実生活で接点のない人のユーザー認証を行うウェブサイトの構築を考えたときに、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 の記述を掲載しちゃってたので修正しました )
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件のフィードバック