miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET MVC クッキーを取得・設定する

ASP.NET MVC でのクッキー(Cookie)の取得・設定する方法についてです。

といっても、ASP.NET Web Forms と同じく HttpCookie(System.Web)を扱いますし、実装自体はとても簡単です。
問題は、使い方とセキュリティ対策です。

  • HTTP のやりとり
  • 例題
  • 実装
  • 大事なこと
  • おまけ:サブクッキー
  • おまけ:モデルバインドでクッキーを取得



by クッキー大好き!クッキーモンスター画像集

HTTP のやりとり

クッキーの値は、クライアント側(ユーザーの PC 側)で保存されます。
ブラウザはサーバー側へリクエストを送信する時に、とある条件でクッキーの値も一緒に送信します。サーバー側はクライアントから送信されてきたクッキーの値を利用することができます。

どのように HTTP のやりとりが行われるかは、第1回 まずは「クッキー」を理解すべし こちらをご覧ください。

例題1

URL「~/Message/Create」でメッセージを入力するフォームを表示し、POST で送信します。

f:id:miso_soup3:20131208051106p:plain

送信後、別のページを適当に表示します。そのあと、ユーザーが URL「~/Message/Create」を GET で再び入力フォームを表示させた時は、最後に送信した内容をフォームの下に表示します。

f:id:miso_soup3:20131208051120p:plain

これをクッキーを使って実装してください。

例題2

例題1の時、URL「~/Home/About」等の別のページへ GET でアクセスした時は、クッキーの値(最後に送信したメッセージの内容)がクライアントからサーバーへ送信しないようにしてください。

実装

プロジェクトの用意

Visual Studio のサイト から、Visual Studio Express 2013 for Web(以下、VS Express 2013)をインストールします。無償です。

VS Express 2013 を開き、メニューから ファイル>新規作成>プロジェクト を選択します。
「ASP.NET Web アプリケーション」を選択し、OK します。

「MVC」を選択し OK します。

モデル(クラス)を用意

入力フォームと、最後に送信した内容を表示するためのモデルを用意します。

using System;
using System.ComponentModel.DataAnnotations;

namespace MyCookieProject.Models
{
	public class CreateMessageModel
	{
		[Required]
		[Display(Name="名前")]
		public string Name { get; set; }

		[Required]
		[Display(Name = "メッセージ")]
		public string Text { get; set; }

		/// <summary>
		/// 最後に登録した名前
		/// </summary>
		[Required]
		[Display(Name = "名前")]
		public string LastName { get; set; }

		/// <summary>
		/// 最後に登録したメッセージ
		/// </summary>
		[Required]
		[Display(Name = "メッセージ")]
		public string LastText { get; set; }

		/// <summary>
		/// 最後に登録した情報が存在するか
		/// </summary>
		/// <returns></returns>
		public bool ExistLastMessageInfo
		{
			get { return !String.IsNullOrEmpty(LastName) || !String.IsNullOrEmpty(LastName); }
		}
	}
}

最後に登録した情報を表示するのは、クッキーが送信されてきた時だけで、最初は表示しません。最後に登録した情報を表示するかどうかを判断するために、ExistLastMessageIngo プロパティを用意しました。

コントローラーの用意

次のアクションを提供するために、MessageController クラスを用意します。

  • GET ~/Message/Create : 入力フォームと最後の登録情報を表示。
  • POST ~/Message/Create : 入力情報をクッキーで送信するよう設定。「完了ページ」へリダイレクトさせる。
  • GET ~/Message/Completed : 完了ページを表示。

MessageController :

using MyCookieProject.Models;
using System.Web;
using System.Web.Mvc;

namespace MyCookieProject.Controllers
{
    public class MessageController : Controller
    {
		[HttpGet]
		public ActionResult Create()
		{
			//クッキーから前回の登録情報を取得します
			var model = new CreateMessageModel()
				{
					LastName = GetCookieValueByKey("LastName"),
					LastText = GetCookieValueByKey("LastText")
				};
			return View(model);
		}

		[HttpPost]
		public ActionResult Create(CreateMessageModel model)
		{
			//入力値を次回からクッキーで送信するよう、レスポンスを設定します
			SetCookie("LastName", model.Name);
			SetCookie("LastText", model.Text);

			return RedirectToAction("Completed");
		}

		[HttpGet]
		public ActionResult Completed()
		{
			return View();
		}

		/// <summary>
		/// キーから、リクエストのクッキーを取得します
		/// </summary>
		/// <param name="key"></param>
		/// <returns></returns>
		private string GetCookieValueByKey(string key)
		{
			HttpCookie cookie = Request.Cookies[key];
			if (cookie == null)
				return null;
			return cookie.Value;
		}

		/// <summary>
		/// レスポンスにクッキーを設定します
		/// </summary>
		/// <param name="key">キー</param>
		/// <param name="value"></param>
		/// <returns></returns>
		private void SetCookie(string key, string value)
		{
			var cookie = new HttpCookie(key);
			cookie.Value = value;
			Response.Cookies.Add(cookie);
		}
	}
}

クッキーの設定と取得は、後ろのほうにヘルパーメソッドを用意したのでそれを使用しています。

最後の送信情報を取得するために、このコントローラーにて、クッキーの値を取得しモデルにセットしています。この時、クッキーの値が存在しない場合は HttpCookie が null になるので注意して下さい。(GetCookieValueByKey のメソッドのところ)

ビューの用意

~/Message/Create と ~/Message/Completed の2つのビューを用意します。単に描画しているだけです。

Create.cshtml :

@model MyCookieProject.Models.CreateMessageModel

@{
	ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
	@Html.AntiForgeryToken()

	<div class="form-horizontal">

		<div class="form-group">
			@Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2" })
			<div class="col-md-10 ">
				@Html.TextBoxFor(model => model.Name, new { @class = "form-control" })
				@Html.ValidationMessageFor(model => model.Name)
			</div>
		</div>

		<div class="form-group">
			@Html.LabelFor(model => model.Text, new { @class = "control-label col-md-2" })
			<div class="col-md-10">
				@Html.TextBoxFor(model => model.Text, new { @class = "form-control" })
				@Html.ValidationMessageFor(model => model.Text)
			</div>
		</div>

		@if (Model.ExistLastMessageInfo)
		{
			<h4>前回登録した情報</h4>

			<div class="form-group">
				@Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" })
				<div class="col-md-10">
					@Html.DisplayFor(model => model.LastName)
				</div>
			</div>
			<div class="form-group">
				@Html.LabelFor(model => model.LastText, new { @class = "control-label col-md-2" })
				<div class="col-md-10">
					@Html.DisplayFor(model => model.LastText)
				</div>
			</div>
		}

		<div class="form-group">
			<div class="col-md-offset-2 col-md-10">
				<input type="submit" value="Create" class="btn btn-default" />
			</div>
		</div>
	</div>
}

<div>
	@Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
	@Scripts.Render("~/bundles/jqueryval")
}

Completed.cshtml :

@{
    ViewBag.Title = "Complete";
}

<h2>完了!</h2>

@Html.ActionLink("Create", "Create")
問題2の対処

上記の実装だけでは、他のページにアクセスした時もクッキーが送信されます。
例えば「~/Home/Contact」にアクセスした時にも、ヘッダーに Cookie が設定されます。↓

GET http://localhost:30626/Home/Contact HTTP/1.1
(略)
Cookie: last-name=たろう; last-text=はろー

あるページにだけクッキーを送信するには、さきほどの MessageController の SetCookie メソッドにて、下のようにパスを設定します。

/// <summary>
/// レスポンスにクッキーを設定します
/// </summary>
/// <param name="key">キー</param>
/// <param name="value"></param>
/// <returns></returns>
private void SetCookie(string key, string value)
{
	var cookie = new HttpCookie(key);
	cookie.Value = value;
	cookie.Path = Request.Path; //現在のリクエストの Path を設定する
	Response.Cookies.Add(cookie);
}

こうすると、サーバーからクライアントへのレスポンスにて、下のように Path 属性がつきます。

HTTP/1.1 302 Found
(略)
Set-Cookie: last-name=たろう; path=/Message/Create
Set-Cookie: last-text=はろー; path=/Message/Create

Path 属性がつくと、ブラウザは指定された Path の時だけクッキーを送信します。クッキーには、このように何種類か属性が用意されています。

大事なこと

実装は以上ですが、クッキーを使用する際は大事なことがいくつもあります。

属性について知る

クッキーには、以下の属性を使用できます。

  • expiress
  • domain
  • path
  • secure
  • httponly

どれも重要な属性なので、仕様を知っておくべきです。特に、secure と httponly はセキュリティ対策で重要な項目です。

コードでは↓のように設定できます。

private void SetCookie(string key, string value)
{
	var cookie = new HttpCookie(key);
	cookie.Value = value;
	cookie.Path = Request.Path;
	cookie.Expires = DateTime.Now.AddDays(20);
	cookie.Domain = "abc.com";
	cookie.Secure = true;
	cookie.HttpOnly = true;
	Response.Cookies.Add(cookie);
}

Web.config でも設定できます。

参考: ASP.NETの状態管理:ビューステート/クッキー/セッション情報

URL エンコード・デコードする

IIS 使用した場合に、クッキーの値が文字化けするようです。参考:ASP.netでクッキー(cookies)を使う時の注意Add Star

また、クッキーの値はクライアント側で好きな値を送信できるため、どんな値がくるかわかりません。なので、↑を参考に URL エンコード・デコードを行います。

クッキーの長さと数に制限がある

ASP.NET での Cookie の基礎 こちらをご覧ください。

おまけ:サブクッキー

ASP.NET では、サブクッキー、つまりサブキーで2階層に分けて値を取得・設定できます。参考:多値 Cookie (サブキー)

設定の仕方はこちら↓

var cookie = new HttpCookie("Food");
cookie.Values["Apple"] = "青森";
Response.Cookies.Add(cookie);

キー Food の下に、キー Apple で”青森”という値を設定しています。
この時、HTTP レスポンスのヘッダーは、↓のようになります。

Set-Cookie: Food=Apple=青森; path=/

上のように設定した場合、”青森”の値を取得する例は、こちら↓

string subCookieValue = String.Empty;
HttpCookie cookie = Request.Cookies["Food"];
if (cookie != null)
{
	subCookieValue = cookie.Values["Apple"];
}

HTTP リクエストのヘッダーは、↓のようになります。

Cookie :  Food=Apple=青森

クッキーの制限個数を節約するのに良いです。

おまけ:モデルバインドでクッキーを取得

最後に、モデルバインドを使用したクッキーの取得方法を紹介して終わります。

NuGet で「Mvc3Futures」をインストールし、Microsoft.Web.Mvc.dll を取得します。
(これ ASP.NET MVC 3 のものなのですが、最新の Microsoft.Web.Mvc.dll を取得するにはどうしたらいいんだろ? →最新版ありました Microsoft.AspNet.Mvc.Futures

f:id:miso_soup3:20131208044909p:plain

Global.asax.cs にてコードを追加します。
using 追加↓

using Microsoft.Web.Mvc;

Application_Start メソッドに↓追加。

ValueProviderFactories.Factories.Add(new CookieValueProviderFactory());

MessageController の Create 画面を表示するメソッドを次のように変更します。

[HttpGet]
[ActionName("Create")]
public ActionResult ViewCreate(CreateMessageModel model)
{
	return View(model);
}

メソッド名を変更して ActionName 属性をつけたのは、POST 用のメソッドと定義がかぶるためです。(メソッド名と引数の型が一緒になっちゃう)
メソッドの中はとくに記述は必要ありません。

f:id:miso_soup3:20131208050236p:plain

実行すればモデルバインドの機能により、引数にクッキーの値が格納されます。
(条件として、クッキーのキーと、引数に定義したクラスのプロパティ名を同じにする必要があります。)

便利ですがどうなんでしょう?
知らない人がコントローラーのコードを見て、このプロパティにはクッキーの値が格納される、って分かるでしょうか?
ValueProvider を指定する属性とかあったら良いですね。(ASP.NET Web API はそこんとこいい感じです。)

おしまい。

ひとこと:
クッキーを使った良い例題が思い浮かばず悩みました・・・。クッキーを使うのは、識別するためくらいでしょうか? 今やサーバー側で保存して、どんなクライアントからでもデータにアクセスできるようになっている気がします。

by 深夜連絡 ASP.NET MVC な Web アプリ Advent Calendar 2013 5 日目