しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

なぞなぞ認証を ASP.NET MVC で実装して遊んでみた

特に意味はないのですが、はてなの各サービスで使えるようになっているなぞなぞ認証を、ASP.NET MVC の機能を使って実装してみました。なぞなぞ認証の詳細ははてなキーワードを見てください。

単純になぞなぞに回答して、それが正解であればページを表示できる機能です。最近は新機能の紹介的なことばかりやってきて疲れてきたので、たまにはこんな実装をしてみるのもいいでしょう。

ASP.NET MVC で実装するので MVC 5 で追加された IAuthenticationFilter を使ってみようと思ったのですが、なぞなぞ認証の場合はユーザーの特定は出来ないので ActionFilter として素直に実装しました。

実装して試した結果

今回はまず実装したらどのような挙動になるのかを紹介しておきます。実際にルートに対してなぞなぞ認証を有効にした場合、専用のフォームが表示されるようになります。

f:id:shiba-yan:20150611013848p:plain

この答えは Azure を使っている方なら誰でも分かると思います。当然ながら答えは kosmosebi ですね。

正しい答えを入力して送信ボタンを押すと、実際のページが表示されるようになります。

f:id:shiba-yan:20150611014207p:plain

これがなぞなぞ認証の大雑把な流れになります。

実際には一度正解した場合には再度入力することの無いように、クッキーに何らかの情報を入れておくべきだと思ったので、適当にクッキーに保存するような処理を実装しました。

コードの解説など

なぞなぞ認証は ActionFilter として実装するので、継承した RiddleAuthenticationAttribute というクラスを作成しました。実装としては簡単で、問題文と答えをコンストラクタで受け取って、ユーザーから答えが入力された場合にはチェックしているだけです。

初回アクセスや、回答が間違っていた場合にはフォームを強制的に表示するようにしています。

public class RiddleAuthenticationAttribute : ActionFilterAttribute
{
    public RiddleAuthenticationAttribute(string question, string answer)
    {
        _question = question;
        _answer = answer;

        var hash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(_question + ":" + _answer));

        _riddleHash = BitConverter.ToString(hash).Replace("-", "").ToLower();
    }

    private readonly string _question;
    private readonly string _answer;

    private readonly string _riddleHash;

    private const char Separator = '|';
    private const string RiddleCookieName = "riddle";

    private const string DefaultAnswerName = "riddle-answer";
    private const string DefaultViewName = "_RiddleForm";

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // 既に回答済みか確認
        var cookie = filterContext.HttpContext.Request.Cookies[RiddleCookieName] ?? new HttpCookie(RiddleCookieName);

        var values = (cookie.Value ?? "").Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries);

        if (values.Contains(_riddleHash))
        {
            return;
        }

        // 回答が入力されたか確認
        var postAnswer = filterContext.HttpContext.Request.Form[DefaultAnswerName];

        if (postAnswer == _answer)
        {
            cookie.Value = string.Join(Separator.ToString(), values.Concat(new[] { _riddleHash }));

            filterContext.HttpContext.Response.SetCookie(cookie);

            return;
        }

        // なぞなぞ認証フォームを表示
        filterContext.Result = new ViewResult
        {
            ViewData = new ViewDataDictionary
            {
                { "Question", _question }
            },
            ViewName = DefaultViewName
        };
    }
}

多少、フォームの項目は決め打ちが多いですが、眠たいのと遊びなのでこのように実装しました。

この属性を使った例は以下のようになります。単純にアクションへ追加するだけです。

public class HomeController : Controller
{
    [RiddleAuthentication("Azure 界の抱かれたい男 No.1 とは?", "kosmosebi")]
    public ActionResult Index()
    {
        return View();
    }
}

ちなみになぞなぞ認証のフォームは以下のような形です。特にいうこともないシンプルなフォームです。

<h2>なぞなぞ認証</h2>

@using (Html.BeginForm())
{
    <div class="form-group">
        <label>@ViewBag.Question</label>
        <input type="text" name="riddle-answer" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">送信</button>
}

実際に使うかと言われると微妙ですが、ActionFilter を使う例としては分かりやすいサンプルになったのではないかと思います。IAuthenticationFilter は Basic 認証の例をどこかでアップしておきたいと思います。