Nancyでのフォーム認証

ナンシーフォーム認証を行うには、以下の流れで行います。

1, Nancy.Authentication.Formsパッケージのインストール
2, IUserMapper実装クラスの作成
3, ログイン、ログアウトルートの実装
4, フォーム認証の設定

IUserMapper実装クラスの作成

IUserMapperインターフェイス、IUserIdentityインターフェイスの実装クラスを作成します。前者はログイン処理で入力されたユーザーID・パスワードのマッピング処理を、後者はユーザー識別情報を表します。NancyではユーザーIDをGUIDで表します。(そのようなNancyの仕様みたい)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Security;

namespace NancyFormAuthSample
{
    public class SampleUserInfo : IUserIdentity
    {
        public IEnumerable<string> Claims
        {
            get;
            set;
        }

        public string UserName
        {
            get;
            set;
        }
    }

    public class UserDatabase : IUserMapper
    {
        //ユーザー情報ストア。サンプルの為、staticなメンバを使用している。
        private static List<Tuple<string, string, Guid>> users = new List<Tuple<string, string, Guid>>();

        public UserDatabase()
        {
            users.Add(new Tuple<string, string, Guid>("admin", "password", Guid.NewGuid()));
            users.Add(new Tuple<string, string, Guid>("user", "password", Guid.NewGuid()));
        }

        public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
        {
            var userRecord = users.Where(u => { return u.Item3 == identifier; }).FirstOrDefault();
            return userRecord == null
                ? null
                : new SampleUserInfo() { UserName = userRecord.Item1 };
        }

        //IUserMapperインターフェイスには含まれていないが、ここでユーザー認証処理を定義している。
        //戻り値として、ログイン入力値に対応するユーザーID値を返す。
        public static Guid? ValidateUser(string username, string password)
        {
            var userRecord = users.Where(u => { return u.Item1 == username && u.Item2 == password; }).FirstOrDefault();

            if (userRecord == null)
            {
                return null;
            }

            return userRecord.Item3;
        }
    }
}

ログイン、ログアウトルートの実装

以下サンプルソースの場合は、
/loginにPOSTリクエストし、ログインを行い、ログイン失敗したら/login?error=trueにリダイレクト。成功したら/secureへリダイレクトします。
/secureは認証済でないとアクセスできません。ログアウトするには/secure/logoutへアクセスします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions;
using Nancy.Security;

namespace NancyFormAuthSample
{
    public class MyModule : NancyModule
    {
        public IndexModule() : base("/login")
        {
            Get["/"] = _ =>
            {
                //クエリ文字列は、Context.Request.Queryで取得する
                if (Context.Request.Query.error != null)
                {
                    return Response.AsText("ユーザーIDまたはパスワードが不正です。", "text/plain; charset=UTF-8");
                }
                else
                {
                    return Response.AsText("ログインする際はPostリクエストしてください。", "text/plain; charset=UTF-8");
                }
            };

            Post["/"] = _ =>
            {
                var userGuid = UserDatabase.ValidateUser(Context.Request.Form.Username, Context.Request.Form.Password);

                if (userGuid == null)
                {
                    //リダイレクト先起点は、モジュールのルートパス起点ではないので注意すること。
                    return Context.GetRedirect("/login?error=true");
                }

                DateTime? expiry = null;
                if (Request.Form.RememberMe.HasValue)
                {
                    expiry = DateTime.Now.AddDays(7);
                }

                return Nancy.Authentication.Forms.ModuleExtensions.LoginAndRedirect(this, userGuid, expiry, "/secure");
            };
        }
    }
    
    public class SecureModule : NancyModule
    {
        public SecureModule()
            : base("/secure")
        {
            //このモジュールルートへのアクセスは認証済である必要あり。
            this.RequiresAuthentication();

            Get["/"] = _ =>
            {
                string msg = string.Format("{0}さん、セキュアなページです。", Context.CurrentUser.UserName);
                return Response.AsText(msg, "text/plain; charset=UTF-8");
            };

            Get["/logout"] = _ =>
            {
                return this.LogoutAndRedirect("/login");
            };
        }
    }
}

フォーム認証の設定

Bootstrapperのサブクラスを作成し、フォーム認証設定を行います。

using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Bootstrapper;
using Nancy.TinyIoc;

namespace NancyFormAuthSample
{
    public class MyBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context)
        {
            base.ConfigureRequestContainer(container, context);
            
            //TinyIoCContainerにIUserMapperの割り付けを登録
            container.Register<IUserMapper, UserDatabase>();
        }

        protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context)
        {
            base.RequestStartup(container, pipelines, context);

            var formsAuthConfiguration = new FormsAuthenticationConfiguration()
            {
                RedirectUrl = "/login", //認証失敗時のリダイレクト先
                UserMapper = container.Resolve<IUserMapper>()
            };
            FormsAuthentication.Enable(pipelines, formsAuthConfiguration);  //フォーム認証の有効化
        }
    }
}

Nancyでの静的リソースアクセス

ナンシー静的リソースへのアクセスを行うには3つの方法があります。

デフォルト設定に従う。

Nancy supports multiple static content conventions at once and is shipped with a default convention that will look for files in the /content path of your application.

https://github.com/NancyFx/Nancy/wiki/Managing-static-content

これが一番簡単な方法です。
/content 以下にリソースファイルを配置するだけです。これはサブディレクトにも適用されます。


リソースディレクトリのカスタム設定(ヘルパー使用)

StaticContentConventionBuilder.AddDirectory()を使って、リソースディレクトリの割り付けを変更しています。

namespace NancyStaticResourceSample
{ 
    public class MyBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ApplicationStartup(Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
        {
            base.ApplicationStartup(container, pipelines);
            this.Conventions.StaticContentsConventions.Clear();  //contentフォルダの割り付け削除
            this.Conventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("/aaa"));
        }
    }
}

リソースディレクトリのカスタム設定

ヘルパークラスが生成するラムダ相当を自前で作成しています。
(以下サンプルはかなりアレなので、参考程度にとどめてください^^;)

namespace NancyStaticResourceSample
{ 
    public class MyBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ConfigureConventions(NancyConventions nancyConventions)
        {
            base.ConfigureConventions(nancyConventions);
            this.Conventions.StaticContentsConventions.Clear();  //contentフォルダの割り付け削除

            nancyConventions.StaticContentsConventions.Add((content, physicalRootPath) =>
            {
                var reqPath = content.Request.Path;

                if (reqPath.StartsWith("/aaa"))
                {
                    var res = new Response();
                    res.Contents = (st) =>
                    {
                        using (var src = new FileStream(System.IO.Path.Combine(physicalRootPath, @"aaa\image1.png"), FileMode.Open))
                        {
                            src.Seek(0, SeekOrigin.Begin);
                            for (var i = 0; i < src.Length; i++)
                                st.WriteByte((byte)src.ReadByte());
                        }
                    };
                    res.ContentType = "image/png";
                    return res;
                }

                return null;
            });
        }
    }
}

リソースディレクトリの無効化

静的リソースを扱わない場合は、リソースアクセス判定処理を無効化することで処理の高速化が見込めるでしょう。

namespace NancyStaticResourceSample
{ 
    public class MyBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ApplicationStartup(Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
        {
            base.ApplicationStartup(container, pipelines);
            container.Register<IStaticContentProvider, DisabledStaticContentProvider>();  //静的リソース判定結果を無条件でfalseとするProviderをRegistする
        }
    }
}

Nancyのトレース機能

ナンシーのトレース機能について調べてみました。

まずはBootstrapperのサブクラスを作成してトレース機能を有効にします。

using Nancy;
using Nancy.Bootstrapper;
using Nancy.TinyIoc;
using Nancy.Diagnostics;

namespace NanNan
{
    public class MyBootstrapper : DefaultNancyBootstrapper
    {
        protected override Nancy.Diagnostics.DiagnosticsConfiguration DiagnosticsConfiguration
        {
            //ダッシュボードへログインする際のパスワードを設定する必要あり。
            get
            {
                return new Nancy.Diagnostics.DiagnosticsConfiguration() { Password = @"Abcd@1234" };
            }
        }

        protected override void ApplicationStartup(Nancy.TinyIoc.TinyIoCContainer container, IPipelines pipelines)
        {
            //トレースを有効にする
            StaticConfiguration.EnableRequestTracing = true;
        }
    }
}


次にモジュールにトレース処理を組み込みます。
これでモジュールの該当ルートへリクエストが発生する度にトレースされるようになります。

using Nancy;
using System;

namespace NanNan
{
    public class MyModule : NancyModule
    {
        public IndexModule()
        {

            Get["/"] = _ =>
                {
                    Context.Trace.TraceLog.WriteLog((s) => 
                    { 
                        s.AppendFormat("ルートへのアクセス:{0}", System.DateTime.Now.Ticks.ToString()); 
                    });

                    return "hello, root";
                };

            Get["/aaa"] = _ =>
            {
                Context.Trace.TraceLog.WriteLog((s) =>
                {
                    s.AppendFormat("aaaへのアクセス:{0}", System.DateTime.Now.Ticks.ToString());
                });

                return "hello, aaa";
            };
        }
    }
}


トレースログを確認するには、ダッシュボードへアクセスします。
<アプリケーションURL>/_Nancy でアクセスすることができます。

1,ログインページ
f:id:ham007:20130729132251p:plain
2,ダッシュボードページ。トラッキングメニューを選びます。
f:id:ham007:20130729132553p:plain
3,ユーザーセッション一覧ページ(と思う)
f:id:ham007:20130729132903p:plain
4,セッション内アクセス一覧ページ
f:id:ham007:20130729132912p:plain
5,アクセス時情報出力ページ
ページ下部にトレース情報が出力されます。
f:id:ham007:20130729132918p:plain



全てのルートへのアクセスに対してトレースしたい場合は、Bootstrapperで設定することが可能。
また、パイプラインのOnErrorでエラー時トレースを行うことも可能。

using Nancy;
using Nancy.Bootstrapper;
using Nancy.TinyIoc;
using Nancy.Diagnostics;

namespace NanNan
{
    public class MyBootstrapper : DefaultNancyBootstrapper
    {
        protected override Nancy.Diagnostics.DiagnosticsConfiguration DiagnosticsConfiguration
        {
            //ダッシュボードを有効にする
            get
            {
                return new Nancy.Diagnostics.DiagnosticsConfiguration() { Password = @"Tonkatu@1919" };
            }
        }

        protected override void ApplicationStartup(Nancy.TinyIoc.TinyIoCContainer container, IPipelines pipelines)
        {
            //トレースを有効にする
            StaticConfiguration.EnableRequestTracing = true;

            pipelines.BeforeRequest += (ctx) =>
            {
            	//Nullを返すことで、リクエスト処理を以降のパイプラインに任せる。
            	//ここで意味のあるレスポンスを返すと、リクエスト処理がこの時点で終了する。
                ctx.Trace.TraceLog.WriteLog((s) => { s.AppendLine("ビフォーリクエスト!!"); });
                return null;
            };

            pipelines.AfterRequest += (ctx) =>
            {
                ctx.Trace.TraceLog.WriteLog((s) => { s.AppendLine("アフターリクエスト!!"); });
            };

            pipelines.OnError += (ctx, err) =>
            {
                ctx.Trace.TraceLog.WriteLog((s) => { s.AppendLine("エラーが発生:" + err.Message); });
                //本当はエラーページのViewなどを返す。
                return err.Message;
            };
        }
    }
}

050plusの留守番電話設定

仕事用の電話番号が欲しかったので、050plus契約してiPhoneで使ってます。
今のところ3G回線でも一部地域を除き、ほぼ問題なく通話できています。*1
そんな感じで完璧ではないけど、まぁいいかなという感じで契約してから半年くらい使ってます。

つい昨日、最も不便だと思ってた留守電機能が標準で搭載されていることを知ったので慌てて設定しました。*2
初期設定は無効化されているので、アプリの管理メニューから留守番電話設定を行う必要があります。
自分が最適だと思える設定まで持っていくのに少し戸惑ったので、メモしておきます。

留守番電話機能の起動条件

050plusアプリが起動している、またはバックグラウンドで生きている場合は、
留守番電話センターへ繋ぎに行きません。こちらが通話開始するor向こうが切るまで延々コールし続けます。
アプリが起動されていない場合に電話がかかってくると、ワンコールくらいで留守番電話センターへ接続し、メッセージ録音ができます。

通知方法

留守番電話設定の「通知方法」を"全て通知"にします。
そうしないと、メッセージが録音されたときだけ着信案内メールが届くようになってしまいます。
f:id:ham007:20130724232445p:plain

音声ファイル

留守番電話設定の「音声ファイル」を"添付する"にします。
メッセージ(が存在する場合)は着信案内メールを受信するだけでメッセージの確認が行えますし、
留守番電話センターでのメッセージ確認がかなり面倒*3なので、この設定1択じゃないでしょうか。
f:id:ham007:20130724232450p:plain
f:id:ham007:20130724232454p:plain

*1:私の場合だけかもしれませんが、上野周辺は雑音がひどく使い物にならなかった。

*2:留守電機能が無いと思い込んでいた。

*3:センタへ電話する→案内言語をプッシュする(日本語or英語)→自分の050電話番号をプッシュする→暗証番号プッシュする→メッセージの確認

ブログの引っ越し(2回目)

やっぱ自分のデータは自分で管理しなきゃ!ということで、はてなダイアリーからレンタルサーバー上のWordPressへ引っ越ししたものの、日々のメンテナンスに疲れてしまいました。そんな折、はてなブログがいい感じになってきてるということを聞きましたので再度の引っ越しをしました。

心機一転がんばるぞ~

新しい非同期呼び出し(async,wait)を試す

.NET4.5から導入される新しい非同期呼び出し。
こんな感じで使うようです。

  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
    }

    //内部で非同期処理を行うメソッドにはasyncをつける
    private async void button1_Click(object sender, EventArgs e)
    {
      //戻り値を返す非同期処理の実行。awaitをつけて非同期処理であることを明示する。
      textbox1.Text = await HeaveyProcess();

      //voidな非同期処理の実行
      StopWatch(5);
    }

    private async Task<string> HeaveyProcess()
    {
      await Task.Delay(TimeSpan.FromSeconds(3.0));
      return "重い処理が完了しました!!";
    }

    private async void StopWatch(int sec)
    {
      Console.WriteLine("開始:{0}", DateTime.Now);
      await Task.Delay(TimeSpan.FromSeconds(sec * 1.0F));
      Console.WriteLine("終了:{0}", DateTime.Now);
    }
  }

複雑なラムダ式

最近、ASP.NETMVCのソースコードを読んでいるのだが、そこでは複雑なラムダ式が多々でてくる。
今回はその一例としてアクションフィルタの処理(ControllerActionInvokerクラスのInvokeActionMethodWithFiltersメソッド)を自分なりに解釈し、コードを起こしたものを載せておく。

いくつか有用そうなテクニックを盗めたし、
ASP.NET MVCソースコードは本当に勉強になります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LinqSample
{
    public interface IActionParameter
    {
        object GetParameter();
    }

    public class GenericParameter<T> : IActionParameter
    {
        private T val;

        public GenericParameter(T val)
        {
            this.val = val;
        }

        public object GetParameter()
        {
            return val;
        }
    }

    public class ActionResult
    {
        public object Value { get; set; }
    }

    public class ActionContext
    {
        public ActionResult CurrentValue { get; set; }

        public ActionContext()
        {
            CurrentValue = new ActionResult() { Value = "" };
        }
    }

    public static class Action
    {
        public static ActionResult ConcatString(Func<ActionResult> action, IActionParameter param)
        {
            ActionResult current = action();
            var newValue = current.Value.ToString() + param.GetParameter().ToString();
            current.Value = newValue;
            Console.WriteLine(current.Value);
          
            return current;
        }

        public static ActionResult GetMaxValue(Func<ActionResult> action, IActionParameter param)
        {
            int tpResult;

            if (!int.TryParse(param.GetParameter().ToString(), out tpResult))
                throw new ArgumentException("パラメーター不正(param)");

            var current = action();

            if (!int.TryParse(current.Value.ToString(), out tpResult))
                throw new ArgumentException("パラメーター不正(action)");

            var newValue = "";

            if (Convert.ToInt32(current.Value) >= Convert.ToInt32(param.GetParameter()))
                newValue = current.Value.ToString();
            else
                newValue = param.GetParameter().ToString();

            Console.WriteLine("現在値:{0}、比較値:{1}  =>  {2}を適用", current.Value, param.GetParameter(), newValue);
            current.Value = newValue;
            return current;
        }
    }

    public class ActionProcesser
    {
        public void DoAction(Func<ActionResult> acumrator, Func<Func<ActionResult>, IActionParameter, Func<ActionResult>> func)
        {
            var parameters = new List<IActionParameter>();
            parameters.Add(new GenericParameter<int>(10));
            parameters.Add(new GenericParameter<int>(20));
            parameters.Add(new GenericParameter<int>(30));
            parameters.Add(new GenericParameter<int>(40));
            parameters.Add(new GenericParameter<int>(50));
            parameters.Add(new GenericParameter<int>(60));
            parameters.Add(new GenericParameter<int>(70));
            parameters.Add(new GenericParameter<int>(80));
            parameters.Add(new GenericParameter<int>(90));
            parameters.Add(new GenericParameter<int>(100));

            /*
             * 第1引数には、アキュムレーター値を返すFuncデリゲートを指定。
             * 第2引数(アキュムレーター関数)には、アキュムレーターと同タイプのFuncを返すデリゲートを指定。
             * こうすることで各パラメーターを外出しすることが可能になる。
             * 
             * アキュムレーター関数にデリゲートを渡すと即時実行されないようなので、最後に()をつけることでInvokeしている。
             */
            parameters.Aggregate(acumrator, (a, p) => func(a, p))();
        }

        public void DoConcatString()
        {
            /*
             * アキュムレーターが保持する値をアキュムレーター自身が保持できない(デリゲートだから)
             * そこで、値保持用のContextクラスを使用することで対応した。
             * 
             */

            var context = new ActionContext();

            Func<ActionResult> acumrator = () =>
            {
                return context.CurrentValue;
            };

            /*
             * 実際の処理呼び出し。
             * ラムダ式の中で、アキュムレーター関数の実処理を呼んでいる。
             * こうすることで実処理のテスト容易になる。
             * (これが一番の狙いか?)
             */
            DoAction(acumrator, (a, p) => () => Action.ConcatString(a, p));
        }

        public void DoCheckMaxValue()
        {
            var context = new ActionContext();

            Func<ActionResult> acumrator = () =>
            {
                context.CurrentValue.Value = "0";
                return context.CurrentValue;
            };

            DoAction(acumrator, (a, p) => () => Action.GetMaxValue(a, p));
        }
    }
}


Action.ConcatStringのテストコード。

[TestClass()]
public class ActionTest
{
        /// <summary>
        ///ConcatString のテスト
        ///</summary>
        [TestMethod()]
        public void ConcatStringTest()
        {
            Func<ActionResult> action = () => new ActionResult() { Value = "やまだ" };
            IActionParameter param = new GenericParameter<string>("たろう");
            ActionResult expected = new ActionResult() { Value = "やまだたろう" };
            ActionResult actual;
            actual = LinqSample.Action.ConcatString(action, param);
            Assert.AreEqual(expected.Value, actual.Value);
        }

        /// <summary>
        ///GetMaxValue のテスト
        ///</summary>
        [TestMethod()]
        public void GetMaxValueTest()
        {
            Func<ActionResult> action = () => new ActionResult() { Value = "100" };
            IActionParameter param = new GenericParameter<int>(99);
            ActionResult expected = new ActionResult() { Value = "100" };
            ActionResult actual;
            actual = LinqSample.Action.GetMaxValue(action, param);
            Assert.AreEqual(expected.Value, actual.Value);
        }
}