CSRF対策として.NET MVC5では、ViewのFormの近くに@Html.AntiForgeryToken()を配置して、コントローラ側でアクションメソッドにValidateAntiForgeryToken属性を付与すればよいだけという手軽さなんだけれど、これformじゃないとダメでAjaxだと同じノリで実装することができない。@Ajax.ActionLinkを利用すればよいそうだが、単純な処理でしか使えそうにないし(汗)

そもそも、AntiForgeryTokenを生成する仕組みはあるわけなので、ちょろっと実装してみることにした。

ヘルパーの作成

まずView側では、@Html.AjaxAntiForgeryToken()と書きたいので、ヘルパーを作成。

AntiForgery.GetTokens()で、クッキー用とフォーム用の認証トークンを生成し、クッキーとフォームに認証トークンを埋め込むだけ。

    public static class AjaxHelper
    {
        /// <summary>
        /// Ajax用のAntiForgeryTokenを埋め込む
        /// </summary>
        /// <param name="helper"></param>
        /// <returns></returns>
        public static MvcHtmlString AjaxAntiForgeryToken(this HtmlHelper helper)
        {
            return AjaxAntiForgeryToken(helper, "ajax-anti-forgery-token");
        }

        public static MvcHtmlString AjaxAntiForgeryToken(this HtmlHelper helper, string id)
        {
            var context = helper.ViewContext.RequestContext.HttpContext;

            string cookieToken, formToken;
            string oldCookieToken = context.Request.Cookies[AntiForgeryConfig.CookieName] == null ? null : context.Request.Cookies[AntiForgeryConfig.CookieName].Value;
            AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);

            if (oldCookieToken == null)
            {
                context.Request.Cookies.Add(new HttpCookie(AntiForgeryConfig.CookieName, cookieToken));
            }
            else
            {
                context.Request.Cookies[AntiForgeryConfig.CookieName].Value = cookieToken;
            }

            var tagBuilder = new TagBuilder("input");
            tagBuilder.Attributes.Add("id", id);
            tagBuilder.Attributes.Add("data-name", AntiForgeryConfig.CookieName);
            tagBuilder.Attributes.Add("value", formToken);
            tagBuilder.Attributes.Add("type", "hidden");

            return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.SelfClosing));
        }
    }

このヘルパーを実行すると、クッキーには

__RequestVerificationToken : クッキー用認証トークン

HTMLには

<input data-name="__RequestVerificationToken" id="ajax-anti-forgery-token" type="hidden" value="フォーム用認証トークン" />

が作成される。

Viewには、

@Html.AjaxAntiForgeryToken()

を適当なところに1つだけ置いてくださいね。(@Html.AntiForgeryToken()との混在はうまくいくのか調査中...クッキーの名前を変えると認証通らなくなるので鍵生成の元になってるっぽい?)

現状、@Html.AntiForgeryToken()との混在はできません。

Valdate用のフィルター作成

お次は認証チェック用のフィルターの作成。これも、アクションメソッドの属性定義としたいので、FilterAttribute, IAuthorizationFilterを継承し、OnAuthorization()メソッドを実装する。

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class ValidateAjaxAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentException("現在の System.Web.HttpContext の値は null です。");
            }

            var httpContext = filterContext.HttpContext;
            if (!(httpContext.Request.HttpMethod == WebRequestMethods.Http.Post || httpContext.Request.HttpMethod == WebRequestMethods.Http.Get))
            {
                throw new WebException("Post/Get以外のHttpMethodは利用できません。", WebExceptionStatus.ProtocolError);
            }

            if (!httpContext.Request.IsAjaxRequest())
            {
                throw new WebException("Ajax通信以外は受け付けません。", WebExceptionStatus.ProtocolError);
            }

            var cookie = httpContext.Request.Cookies[AntiForgeryConfig.CookieName];
            if (httpContext.Request.HttpMethod == WebRequestMethods.Http.Post)
            {
                AntiForgery.Validate(cookie != null ? cookie.Value : null, httpContext.Request.Form[AntiForgeryConfig.CookieName]);
            }

            if (httpContext.Request.HttpMethod == WebRequestMethods.Http.Get)
            {
                AntiForgery.Validate(cookie != null ? cookie.Value : null, httpContext.Request.Params[AntiForgeryConfig.CookieName]);
            }
        }
    }

今回は、POST専用なのでRequest.Formから認証情報を取得してるけど、GETの場合は、Request.Paramsの方にデータが入ってくるので要注意!
リクエストヘッダにセットしている例も結構あったけれど、Javascript側のコードがちょい複雑になるので単純にリクエストデータに認証情報をセットする前提。

やってることはといえば、クッキーとリクエストから取得したそれぞれの認証トークンをAntiForgery.Validate()してるだけ。

アクションメソッドでの利用方法は、属性付与のみ。

        [HttpPost]
        [ValidateAjaxAntiForgeryToken]
        public ActionResult PnrFareQuote(RequestArgs args)
        {

話はそれるが、クッキー用認証トークンとフォーム用の認証トークンは異なる文字列。同じかどうか比較してるんじゃないのであしからず(笑)

Ajaxでのリクエスト方法

リクエストパラメータにフォーム用認証トークンをセットしてあげるだけ。

var data = {
            ID: $('#hoge-id').val(),
            TypeNo: $('.hoge-type-option:checked').val(),
            Code: $('#hoge-code').val(),
        };

        data[$('#ajax-anti-forgery-token').data('name')] = $('#ajax-anti-forgery-token').val();

        $('#results').load('/コントローラ名/アクション名', data,
            function () {
              //コールバック

上記のサンプルちょっとカッコ悪いんだけど、HTMLに埋め込んだ

<input data-name="__RequestVerificationToken" id="ajax-anti-forgery-token" type="hidden" value="フォーム用認証トークン" />

...の、data-nameがリクエストパラメータ名で、valueがフォーム用認証トークンとなるので、やり方はどうであれそいつをリクエストパラメータに追加してあげればOK。

欲を出してヘルパー側でオブジェクト作ってあげようかな?とか思ったけど$extendしてくれればいいけどそうでないと足手まといになりそうなので、今回はやめときました(汗)

参考にしたサイト:http://stackoverflow.com/questions/14925446/how-to-get-antiforgerytoken-value-without-hidden-input