2016年12月28日水曜日

ASP.NET MVC 13_モデルバインド

VisualStuidioCommunity2015/Fw4.5.2/C#


前回まではモデルの値をビューに表示する方法を見てきました。
今回はビューの値をコントローラーで受け取る方法を見ていきたいと思います。

モデルバインド

モデルバインドとはクライアントからの入力値を、アクションメソッドの引数に自動的に割り当てる機能のことです。
ポストデータやクエリーパラメータ、ルートパラメータなど、リクエストデータと同名の引数(大文字小文字は区別されない)をアクションメソッドに用意しておくだけで、自動的に割り当ててくれます。

まずはモデルバインドの基本的な動作を確認してみます。
以下の例では、ビューに用意したテキストボックス「StringValue」で入力した値が、アクションメソッド「SimpleBind」に用意された同名の引数「stringvalue」にバインドされます。

ビュー
@using (Html.BeginForm("SimpleBind", "ModelBind"))
{
    <dl>
        <dt>@Html.Label("String値:")</dt>
        <dd>@Html.TextBox("StringValue")</dd>
    </dl>
    <div>
        <input type="submit" value="送信" />
    </div>
}
コントローラー
using System.Web.Mvc;

namespace Practice.Controllers
{
    public class ModelBindController : Controller
    {

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

        public ActionResult SimpleBind(string stringvalue)
        {
            string value = $"「{stringvalue}」が入力されました。";
            return Content(value);
        }
    }
}



上記の例はアクションメソッドの引数がstring型でしたが、int型に変えてみます。

ビュー
@using (Html.BeginForm("SimpleBind", "ModelBind"))
{
    <dl>
        <dt>@Html.Label("int値:")</dt>
        <dd>@Html.TextBox("IntValue")</dd>
    </dl>
    <div>
        <input type="submit" value="送信" />
    </div>
}
コントローラー
using System.Web.Mvc;

namespace Practice.Controllers
{
    public class ModelBindController : Controller
    {

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

        public ActionResult SimpleBind(int intvalue)
        {
            string value = $"「{intvalue}」が入力されました。";
            return Content(value);
        }
    }
}
テキストボックスに何も入力せずに送信したり、数値に変換できない文字を入力して送信すると、「アクションパラメータの引数にnullが設定できないよ」とエラーになってしまいます。

モデルバインダーは、リクエストデータに値が設定されていないときや、アクションメソッドの同名の引数に指定した型に変換できない場合は、引数にnullを設定しようとします。

アクションメソッドの引数が値型の場合、nullを受け入れられるようにnullable型にしておく必要があります。
public ActionResult SimpleBind(int? intvalue)
{
    string value = $"「{intvalue}」が入力されました。";
    return Content(value);
}


アクションメソッドの引数にモデルを指定すると、リクエストデータと同名のプロパティに値をセットしてくれます。
モデルのプロパティが値型の場合、必須でなければnullableにしておきます。

モデル
using System.ComponentModel.DataAnnotations;

namespace Practice.Models
{
    public class BindSampleViewModel
    {
        [Display(Name = "string値")]
        public string StringValue { get; set; }

        [Display(Name = "int値")]
        public int? IntValue { get; set; }
    }
}
ビュー
@model Practice01_Begin.Models.BindSampleViewModel

@using (Html.BeginForm("BindSampleResult", "ModelBind"))
{
<dl>
    <dt>@Html.DisplayNameFor(mdl => mdl.StringValue)</dt>
    <dd>@Html.EditorFor(mdl => mdl.StringValue)</dd>
    <dt>@Html.DisplayNameFor(mdl => mdl.IntValue)</dt>
    <dd>@Html.EditorFor(mdl => mdl.IntValue)</dd>
</dl>
<input type = "submit" value = "送信" />
コントローラー
using System.Web.Mvc;

namespace Practice.Controllers
{
    public class ModelBindController : Controller
    {
        public ActionResult BindSample()
        {
            return View();
        }
        public ActionResult BindSampleResult(Models.BindSampleViewModel mdl)
        {
            string value = $"「{mdl.StringValue},{mdl.IntValue}」が入力されました。";
            return Content(value);
        }
    }
}


オーバーポスティング攻撃の対策

アクションメソッドの引数にモデルを指定する場合は「オーバーポスティング攻撃」を受ける可能性が出てきます。
たとえば以下のように、モデルにビューに表示するだけのプロパティ「ImportantValue」があったとします。
using System.ComponentModel.DataAnnotations;

namespace Practice01_Begin.Models
{
    public class BindSampleViewModel
    {
        [Display(Name = "string値")]
        public string StringValue { get; set; }

        [Display(Name = "int値")]
        public int? IntValue { get; set; }

        [Display(Name = "権限")]
        public string Role { get; set; }
    }
}
ビューはStringValueとIntValueのみが編集でき、Roleは表示するだけになっていたとします。
@model Practice01_Begin.Models.BindSampleViewModel

@using (Html.BeginForm("BindSampleResult", "ModelBind"))
{
<dl>
    <dt>@Html.DisplayNameFor(mdl => mdl.StringValue)</dt>
    <dd>@Html.EditorFor(mdl => mdl.StringValue)</dd>
    <dt>@Html.DisplayNameFor(mdl => mdl.IntValue)</dt>
    <dd>@Html.EditorFor(mdl => mdl.IntValue)</dd>
    <dd>@Html.DisplayNameFor(mdl => mdl.Role)</dd>
    <dd>@Html.DisplayFor(mdl => mdl.Role)</dd>
</dl>
<input type = "submit" value = "送信" />
コントローラーは先ほどと同じで、引数にモデルを指定します。
ただし、モデルのRoleの値が’admin’であれば、何かしら重要な処理をするとします。
このとき悪意あるユーザーがポストデータのRoleを「admin」に改ざんしたデータを送信すると、意図しない動きになってしまいます。
using System.Web.Mvc;

namespace Practice.Controllers
{
    public class ModelBindController : Controller
    {
        public ActionResult BindSample()
        {
            var mdl = new Models.BindSampleViewModel();
            mdl.StringValue = "StringValue";
            mdl.IntValue = 12345;
            mdl.Role = "user";
            return View(mdl);
        }  
       public ActionResult BindSampleResult(Models.BindSampleViewModel mdl)
 {
     if (mdl.Role != null)
     {
         //"重要な処理をする";
     }
     string str = $"「StringValue:={mdl.StringValue},IntValue:={mdl.IntValue}, Role:={mdl.Role}」が入力されました。";
     return Content(str);
     }
 }
}
自動的にバインドしたくないプロパティがある場合は、
「バインドするプロパティを明示的に指定する」か「バインドしないプロパティを指定する」ことでオーバーポスティング攻撃を防御できます。

バインドするプロパティを明示的に指定するには、
コントローラーのアクションメソッドの引数にBind属性をつけ、Includeプロパティにバインドするプロパティ名を設定します。
public ActionResult BindSampleResult
([Bind(Include = "StringValue, IntValue")] Models.BindSampleViewModel mdl)
{
    ・・・略
}
バインドしないプロパティを指定するには、
コントローラーのアクションメソッドの引数にBind属性をつけ、Excludeプロパティにバインドしないプロパティ名を設定します。
public ActionResult BindSampleResult
([Bind(Exclude = "Role")] Models.BindSampleViewModel mdl)
{
    ・・・略
}
ためしにビューでRoleプロパティを入力できるようにして実行してみます。

Bind属性をつけるとRoleには入力値がバインドされていません。

Bind属性を外すとRoleniha入力値がバインドされています。


IncrudeプロパティかExcludeプロパティのどちらを使用すればよいか、参考にしている本「実践プログラミング ASP.NET MVC5」では、原則としてIncludeプロパティを利用すべきとの記載があります。
Excludeプロパティではあとからプロパティを追加した場合、Exclude定義の更新を忘れて意図しないバインドを受け入れてしまう可能性があり
Includeプロパティではあとからプロパティを追加した場合、Include定義の更新を忘れても、値がバインドされていないという明確な結果になり、問題もすぐ特定できるためとありました。

0 件のコメント: