ASP.NET Core 2.0 MVC で Cookie を利用する

# 開発・動作確認環境
- Visual Studio 2017 v 15.4.3
- ASP.NET Core Webアプリケーション MVC
- .NET Core 2.0
- Fiddler v5.0.20173.48897 for .NET 4.6.
- Google Chrome 62.0.3202.94

# Cookieの書き込み

## 基本形

```c#:CookieController.cs
using Microsoft.AspNetCore.Mvc;
namespace AspDotNetCore2_Cookie.Controllers {

  public class CookieController : Controller {

    private const string cKey = "TestKey";
    private string cValue = "TestValue";

    public IActionResult Write() {
      this.HttpContext.Response.Cookies.Append(cKey, cValue);
      return Ok();
    }

  }
}
```

アクセスがあったときにブラウザにクッキーを書き込む（保存する・セットする）ためには、コントローラー上で`this.HttpContext.Response.Cookies.Append`(`IResponseCookies.Append`)メソッド
を呼び出します。`IResponseCookies.Append`の引数はstring型で与えます。
上記をビルドして`/cookie/write`にアクセスしたときの**レスポンス**のヘッダをFiddlerを使って調べると、次のような`Set-Cookie`ヘッダが付加されていることが確認できます。Cookieは「Key=Value」の形式でセットされています。なお、pathはURLに関係なく（ここでは`/cookie/write`にアクセスしましたが）、ルート`/`に設定されるようです。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey=TestValue; path=/
```

書き込まれたCookie情報をブラウザで確認します。Google Chromeでは「設定」－「詳細設定」－「コンテンツの設定」－「Cookie」－「すべての Cookie とサイトデータを表示」から、保存されているCookie情報を表示することができます。

![クッキーの確認.png](https://qiita-image-store.s3.amazonaws.com/0/152805/fa2d7645-c401-e95c-bee5-e70f22a4a467.png)

次に、ブラウザから`/home/index`にアクセスしてみます。そのときの**リクエスト**ヘッダをFiddlerを使って調べると、次のように`Cookie`ヘッダが付加されていることが確認できます。`Set-Cookie`でパスがルート`/`に設定されていたので、同じドメイン（ここではlocalhost）の`/`以下のパス（例えば`home/index`）にアクセスしたときもCookieが送信されています。
※`Set-Cookie`に任意のパスを設定する方法は後述

```x:HttpRequestHeader（抜粋）
Cookie: TestKey=TestValue
```

## 複数のCooikeの書き込み

次のように、2つのCookie情報を書き込んだときの動作を確認します。

```c#:CookieController.cs（抜粋）
public IActionResult Write() {
  this.HttpContext.Response.Cookies.Append("TestKey1", "TestValue1");
  this.HttpContext.Response.Cookies.Append("TestKey2", "TestValue2");
  return Ok();
}
```

**レスポンスヘッダ**をFidllerで確認すると、次のような形式で2行のヘッダが追加されていることが確認できました（`Set-Cookie`ヘッダは複数行になる）。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey1=TestValue1; path=/
Set-Cookie: TestKey2=TestValue2; path=/
```

次に、上記Cookieがブラウザに保存されている状態での**リクエスト**ヘッダを確認すると、次のようになりました。セミコロンで連結された情報が**1行**のヘッダとして付加されています。

```x:HttpRequestHeader（抜粋）
Cookie: TestKey1=TestValue1; TestKey2=TestValue2
```

# Cookieの読み込み

次のように`this.HttpContext.Request.Cookies[KEY]`で、リクエストメッセージ内のCookieの値を読み込むことができます。該当のキーが存在しないときはnullが戻ってきます。 

```c#:CookieController.cs
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;

namespace AspDotNetCore2_Cookie.Controllers {

  public class CookieController : Controller {

    private const string cKey = "TestKey";
    private string cValue = "TestValue";

    public IActionResult Read() {
      string foo = this.HttpContext.Request.Cookies[cKey];
      Debug.WriteLine(cKey + "=" + (foo?.ToString() ?? "[NULL]"));
      return Ok();
    }

    public IActionResult Write() {
      this.HttpContext.Response.Cookies.Append(cKey, cValue);
      return Ok();
    }
  }
}
```

また、次のように`HttpContext.Request.Headers["Cookie"]`を使っても、リクエストに含まれるCookieを直接読み込むことができます。

```c#:CookieController.cs（抜粋）
public IActionResult Read() {
  string foo = this.HttpContext.Request.Headers["Cookie"];
  Debug.WriteLine((foo?.ToString() ?? "[NULL]"));
  return Ok();
}
```

なお、この場合、複数のCookieがあると変数fooには`TestKey1=TestValue1; TestKey2=TestValue2`のように複数のCookie情報が連結された文字列が入るので、その後の処理が面倒になります。

## Cookieに日本語を設定してみる

次のようにTestValueに日本語を入れてみます。`CookieController.cs` は「Unicode(UTF-8シグネチャ付き) 65001」で保存しました。

```c#:CookieController.cs（抜粋）
public IActionResult Write() {
  this.HttpContext.Response.Cookies.Append("TestKey", "ほげほげ");
  return Ok();
}
```

`/cookie/write`にアクセスしたときのレスポンスの`Set-Cookie`ヘッダは次のようになりました。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey=%E3%81%BB%E3%81%92%E3%81%BB%E3%81%92; path=/
```

ここで「%E3%81%BB%E3%81%92%E3%81%BB%E3%81%92」は、UTF-8の文字列「ほげほげ」をURLエンコードした文字列になります。Fiddlerの「Tools」－「TextWizard」で確認できます。

![encode.png](https://qiita-image-store.s3.amazonaws.com/0/152805/fb7c5bab-5b8f-ab66-7845-fdec1974a136.png)

つづいて、このCookie情報を`HttpContext.Request.Cookies["TestKey"]`で読み込んでみます。その結果、デバッグ出力は「ほげほげ」（文字化けなしの日本語）になりました。`Response.Cookies'と'Request.Cookies`では、自動的にURLエンコード、デコードをしてくれるようです。
一方、`HttpContext.Request.Headers["Cookie"]`を使って読み込むと、デコードされていない生の「%E3%81%BB%E3%....」という値が取得できます。

# Cookiesの詳細オプション設定

レスポンスメッセージのなかのSet-Cookieヘッダに各種オプションを設定したいときは、`Response.Cookies.Append`の第3引数に`Microsoft.AspNetCore.Http.CookieOptions`をセットします。

## CookieOptions（デフォルト）

`CookieOptions`のデフォルト設定から確認します。なお、`CookieOptions`を利用する際は、名前空間`using Microsoft.AspNetCore.Http`を追加しておきます。

```c#:CookieController.cs（抜粋）
public IActionResult Write() {
  var cOptions = new CookieOptions();
  this.HttpContext.Response.Cookies.Append(cKey,cValue,cOptions);
  return Ok();
}
```

レスポンスのSet-Cookieヘッダを確認すると`samesite=lax`が追加されていることが分かります。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey=TestValue; path=/; samesite=lax
```

`samesite=lax`について日本語の情報が少なくあやふやですが、この属性があるとクロスドメインアクセスしたときに当該Cookieを送信しなくなるようです。ChromeでCookie情報を確認すると、次のように送信先が「あらゆる種類の接続」から「同サイトの接続のみ」に変わっていました。

![送信先.png](https://qiita-image-store.s3.amazonaws.com/0/152805/834279c8-ae9c-b594-afd5-1eab4d07672b.png)

もう少し実験します。次のようなファイルをデスクトップ上に作成してブラウザにドラッグ＆ドロップします。アプリをデプロイしているドメイン（localhost）とは異なるドメイン（file:///C:/Users/xxxx/Desktop/test.html）に設定したJavaScriptから`/cookie/read`にアクセスするサンプルです。

```haml:test.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
  <script>
    var req = new XMLHttpRequest();
    req.open('GET', 'http://localhost:8080/cookie/read');
    req.withCredentials = true;
    req.send(null);
  </script>
</body>
</html>
```

Fiddlerで確認すると、`samesite=lax`が**設定されている**ときにはJSからのリクエストにCookieが含まれていませんでした。一方、`samesite=lax`が**設定されていない**ときには、リクエストにCookieが含まれていました。なお、`req.withCredentials=true`がないと、どちらの場合でもCookieを送信しないようです。

## CookieOptions.Path（送信先の設定）

同一ドメイン内でのCookie送信先を制限するためには、次のようにPathプロパティを設定します。Pathプロパティはstring型でセットします。ここでは`/cookie`配下にアクセスするときのみ、cookieを送信するように設定しました。

```c#:CookieController.cs（抜粋）
private const string cKey = "TestKey";
private string cValue = "TestValue";

public IActionResult Write() {
  var cOptions = new CookieOptions() {
    Path="/cookie"
  };
  this.HttpContext.Response.Cookies.Append(cKey,cValue,cOptions);
  return Ok();
}
```

上記`/cookie/write`にアクセスしたときのレスポンスのヘッダは次のようになります。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey=TestValue; path=/cookie; samesite=lax
```

この状態でのcookie送信の有無は次のようになります。

- `/cookie/read` → 送信される
- `/cookie` → 送信される
- `/` →　送信されない
- `/home/index` →　送信されない
- `/Cookie/read` → **送信されない** ※コントローラー名を大文字で指定 

## CookieOptions.Expires（有効期限の設定）

CookieOptionsを与えずに`Cookies.Append`した場合や、CookieOptionsのデフォルト値を与えて`Cookies.Append`した場合、そのCookieの有効期限は「ブラウザセッションの終了時」つまり、そのブラウザを閉じるまでに設定されました。Expiresプロパティを与えると、その有効期限を任意に設定することができます。ExpiresプロパティはDateTimeOffset型でセットします（DateTime型ではない）。
ここでは、アクセス時刻＋2日を有効期限に設定しています。

```c#:CookieController.cs（抜粋）
private const string cKey = "TestKey";
private string cValue = "TestValue";

public IActionResult Write() {
  var cOptions = new CookieOptions() {
    Path="/cookie",
    Expires = new DateTimeOffset(DateTime.Now.AddDays(2))
  };
  this.HttpContext.Response.Cookies.Append(cKey,cValue,cOptions);
  return Ok();   
```

上記`/cookie/write`に、2017/12/08 Fri **09:34:49** にアクセスしたときのレスポンスのヘッダは次のようになります。Cookieの有効期限は、GMTグリニッジ標準時でセットされるようです（日本時間と9時間ずれています）。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey=TestValue; expires=Sun, 10 Dec 2017 00:34:49 GMT; path=/cookie; samesite=lax
```

また、ブラウザでCookie情報を確認すると日本時間になっています（ユーザーに分かりやすいようにブラウザが変換して表示していると思われます）。

![期限.png](https://qiita-image-store.s3.amazonaws.com/0/152805/d709afc9-114e-9069-63f0-83b4925007c0.png)

## CookieOptions.HttpOnly（JSからの参照制限）

JavaScriptからのCookie情報参照を制限するための設定です。CookieOptionsのExpiresプロパティを`true`にセットします。

```c#:CookieController.cs（抜粋）
    private const string cKey = "TestKey";
    private string cValue = "TestValue";
    public IActionResult Write() {
      var cOptions = new CookieOptions() {
        HttpOnly = true
      };
      this.HttpContext.Response.Cookies.Append(cKey,cValue,cOptions);
      return Ok();
    } 
```

レスポンスを確認すると`Set-Cookie`にhttponly属性がつきました。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey=TestValue; path=/; samesite=lax; httponly
```

次のようなHTMLファイルを同じドメインのwwwrootの下に配置して動作を確認します。

```haml:test.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
</head>
<body>
  <script>
    var cookies = document.cookie;
    console.log(cookies);
  </script>
</body>
</html>
```

httponly属性がついていないときはコンソールログに次のようにcookie情報が表示されますが、httponly属性がつくと表示されなくなります。

![jsc.png](https://qiita-image-store.s3.amazonaws.com/0/152805/f84d2703-1349-466f-805f-bf8a419b780a.png)

ただし、httponly属性がついていても、JSからのAjaxの際にCookieがつかなくなるわけではないようです。

```haml:test.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
</head>
<body>
  <script>
    var cookies = document.cookie;
    console.log(cookies);
    var req = new XMLHttpRequest();
    req.open('GET', '/cookie/read');
    req.withCredentials = true;
    req.send(null); // この通信ではCookieが送信される
  </script>
</body>
</html>
```

# Cookieの削除

Cookieの削除には`HttpContext.Response.Cookies.Delete()`を使います。

```c#:CookieController.cs
using System;
using Microsoft.AspNetCore.Mvc;

namespace AspDotNetCore2_Cookie.Controllers {
  public class CookieController : Controller {
    private const string cKey = "TestKey";
    public IActionResult Delete() {
      this.HttpContext.Response.Cookies.Delete(cKey);
      return Ok();
    }
  }
}
```

レスポンスのヘッダは次のようになります。保持期限を過去時刻にして上書きすることで実質的な削除を行なっています。

```x:HttpResponseHeader（抜粋）
Set-Cookie: TestKey=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax
```


# 参考資料

- Microsoft.AspNetCore.Http.CookieOptions の[リファレンス](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev15.query%3FappId%3DDev15IDEF1%26l%3DJA-JP%26k%3Dk(Microsoft.AspNetCore.Http.IResponseCookies.Append);k(DevLang-csharp)%26rd%3Dtrue&view=aspnetcore-2.0)