ITP2.1対策　Safari 12.1 でCookieの有効期限を8日以上に延長する方法

# Safari 12.1に搭載されるITP2.1について
2019年3月25日、Safari 12.1がmacOS、iOSでリリースされました。
~~2019年3月1日現在、Safari 12.1はBeta3が配信されています。
正式版がAppStore経由で配信されるのは、もう間もなく、という段階でしょう。~~
Safari 12.1にはITP（Intelligent Tracking Prevention）の最新版であるITP2.1が搭載されますが、
ITP2.1ではCookieの挙動に対して、従来以上に厳しい制約が課されることが明らかになりました。

ITP、およびITP2.1の詳細については他の記事を参照いただくことにして、
本記事ではファーストパーティCookieの有効期限対策にフォーカスします。

## ファーストパーティCookieへの制限
2019年2月21日にポストされた公式ブログの記事です。
[Intelligent Tracking Prevention 2.1](https://webkit.org/blog/8613/intelligent-tracking-prevention-2-1/)
https://webkit.org/blog/8613/intelligent-tracking-prevention-2-1/
以下はCookie有効期限に対して言及している部分
>Cookies can either be set in HTTP responses or through the document.cookie API, the latter sometimes referred to as client-side cookies. 
With ITP 2.1, all persistent client-side cookies, i.e. persistent cookies created through document.cookie, are capped to a seven day expiry.

要約すると、
**Javascriptで作成するCookieの有効期限は、最大7日に制限する。**
ということです。
そしてこれは**ファーストパーティCookie**の話です。

記事がポストされたのは4月1日ではないので、どうやら本気のようです。


## Cookie作成時の挙動
実際にITP2.1を搭載したSafariがファーストパーティCookieをどのように処理するのか調べました。
以下の内容は、2019年3月1日現在のmacOS版 Safari 12.1 Beta3の挙動に基づくものです。

| Cookie作成方法 | 指定した有効期限 | 設定された有効期限 |
|:--------------|:---------------:|:----------------:|
| クライアントサイド | なし | なし |
| クライアントサイド | 7日以内の日時 | 7日以内の日時 |
| **クライアントサイド** | **8日以上先の日時** | **7日後の日時** |
| サーバーサイド | なし | なし |
| サーバーサイド | 7日以内の日時 | 7日以内の日時 |
| サーバーサイド | 8日以上先の日時 | 8日以上先の日時 |
※クライアントサイド ＝ Javascriptのdocument.cookieで作成。
※サーバーサイド ＝ レスポンスヘッダーのSet-Cookieで作成。
※**いずれも、HttpOnlyフラグ無し、Secureフラグ無し。**

この結果は、先の公式ブログで言及されていたとおりの挙動です。
**クライアントサイド（Javascript）で作成したCookieのみ、有効期限が最大で7日に制限されています。
サーバーサイド（Set-Cookie）で作成されるCookieは制限を受けません。**

参考画像： Safari 12.1 Beta3 でBingのCookieを表示。
Javascriptが作成したCookie "SRCHHPGUSR" の有効期限は、表示日時から7日後に制限されている。
その他のサーバーサイドCookieは有効期限の制限を受けない。
<img width="1136" alt="Safari12.1set-cookie.png" src="https://qiita-image-store.s3.amazonaws.com/0/136978/f4e69a8d-ce22-2bc0-252e-27736b6e49cd.png">


## ITP2.1の影響
ITP2.1が本来ターゲットとしているのは、主に広告トラッキングに対する制限ですが、
今回の制限はそれ以外のCookieも巻き添えになっている感が否めないと思います。

WebサイトやWebアプリケーションで、ユーザーを補助する機能や表示分岐のために
クライアントサイドCookieが使われることはよくあります。
今回の制限はこれらの実装を直撃しています。
各Webサイトの担当者は、どのような影響を受けるのか精査する必要があると思います。

そして悩ましいのは、この制限は今のところSafariだけの問題であり、
Chrome、Firefox、IE、Edgeなど、他のブラウザには関係がないということです。
しかし、SafariはmacOSだけではなく、日本のスマホ市場で大きいシェアを持つiPhoneに搭載されている
iOSの標準ブラウザでもありますので、この影響は無視できないと思われます。


# ITP2.1の「Cookie7日問題」の回避策
ここからは、Javascript（document.cookie）で作成されるファーストパーティCookieの有効期限が
**最大７日に制限される問題を回避する**ための対策をいくつか挙げます。

以下に挙げる対策は、
WebサイトやWebアプリケーションで独自にCookieを作成しているケースへの対策であり、
アクセス解析ツールやWebマーケティング系ツールに向けた対策ではありません。　

**なお、LocalStorageなどのCookie以外の技術に乗り換えが可能ならば、
この機会にCookieをやめることをオススメします。**
やむを得ずCookieの継続が必要である場合に限り、回避策の検討をするべきでしょう。

## １．Webサーバー側でSet-Cookieする
ITP2.1で有効期限が制限されるCookieは、Javascript（document.cookie）で作成されるCookieです。
Webサーバーからの**レスポンスヘッダー Set-Cookie で作成されたファーストパーティCookieは、
有効期限の制限を受けません。**
これが最善の対策になります。

しかし、Webサーバー側でSet-Cookieが出来るならば、始めからそうしているでしょう。
なんらかの理由により、CookieをJavascriptで作成する必要があるならば、次の方法がオススメです。

## ２．AjaxでSet-Cookieヘッダーをオウム返しする
Cookieに保存する値をJavascript側で生成し、それをWebサーバー側からSet-Cookieする方法です。

これにはAjax（XMLHttpRequest）とサーバーサイドプログラム（PHPなど）を利用します。
Javascriptで生成したCookieの値をAjaxでWebサーバーに送り、
サーバーサイドプログラムでSet-Cookieをオウム返しする。
これで有効期限の制限を受けることなくCookieを作成できます。

**ただし、サーバーサイドプログラムは、同じドメインかサブドメインの範囲内に設置することが前提です。**
他ドメインからSet-Cookieを受ける場合はサードパーティ扱いになるので利用できません。
なお、AjaxでサブドメインのCORS制約を超える場合は、以下の２つのヘッダーを併用することでSet-Cookieが可能になります。
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
このあたりについては、以下の記事が参考になります。
[CORSリクエストでクレデンシャル(≒クッキー)を必要とする場合の注意点](https://qiita.com/kawaz/items/1e51c374b7a13c21b7e2)
※この場合はCookieのドメイン名に親ドメインを指定して、サブドメイン間でCookieを共有する。

### クライアントサイドでCookieの値を用意してサーバーへ送信
まず、Cookieの値をJavascriptで生成します。たとえば以下のような値です。
`Name=Value; expires=Mon, 1 Jan 2029 12:00:00 GMT; domain=example.com; path=/`
ここまでは従来と同じです。
その値をAjaxでサーバーサイドプログラムに送信します。
以下は、テスト用のフォームを含むHTMLとAjaxスクリプトです。

```html:setcookie.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Ajax Set Cookie</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
</head>
<body>
    <form onsubmit="return false;">
        Cookie Value:
        <input type="text" name="cookie_value" id="cookie_value" size="70"
         value="Name=Value; expires=Mon, 1 Jan 2029 12:00:00 GMT; domain=example.com; path=/">
        <!-- domain=example.comは書き換えてください -->
        <input type="submit" name="set_cookie" id="set_cookie" value="Set Cookie">
    </form>
    <div id="result"><!-- debug info --></div>
    <div id="cookies"><!-- debug info --></div>
    <script type="text/javascript">
        $(function(){
            // #set_cookie click
            $('#set_cookie').on('click', function(){
                // request of set-cookie callback
                $.ajax({
                    type:'POST',
                    url:'./setcookie.php',
                    xhrFields:{
                        // config for CORS
                        'withCredentials': true
                    },
                    data:{
                        // set cookie value
                        'cookie_value': $('#cookie_value').val()
                    }
                })
                .done( (response) => {
                })
                .fail( (response) => {
                })
                .always( (response) => {
                    // print debug info
                    $('#result').html('Result: ' + response);
                    $('#cookies').prepend('<p>Cookies: ' + document.cookie + '</p><hr>');
                    console.log('Result: ' + response);
                    console.log('Cookies: ' + document.cookie);
                });
            });
        });
    </script>
</body>
</html>
```

### サーバーサイドでオウム返しする
サーバーサイドプログラムの処理もシンプルです。
ブラウザから受け取ったCookieの値をレスポンスヘッダーのSet-Cookieに設定するだけです。
以下はPHPのサンプルです。他の言語でも簡単に実装できる内容です。

```php:setcookie.php
<?php
    header('Content-type: text/plain; charset=UTF-8');
    header('Access-Control-Allow-Credentials: true'); // config for CORS
    header('Access-Control-Allow-Origin: https://www.example.com'); // config for CORS
    if( isset($_POST['cookie_value']) && $_POST['cookie_value'] != '' ){
        //入力されたcookie_valueで意図しない処理が実行されないよう、危険な文字列をエスケープまたは除去してください。
        header('Set-Cookie: ' . $_POST['cookie_value'] );
        echo 'SUCCESS';
    }
    else{
        echo 'FAIL';
    }
?>
```

この方法なら、Javascriptを起点として作成したCookieでありながら、
有効期限を自由に設定できます。1ヵ月後でも、1年後でも、10年後でも可能です。

しかし、サーバーサイドプログラムの設置ができない場合、この方法は使えません。
次はクライアントサイド（Javascript）のみで対策する方法です。


## ３．LocalStorageに保存したデータでCookieを作成する
データをCookieに保存するのではなく、WebStorage APIでLocalStorageに保存しておきます。
LocalStorageならば、期限の制限なく永続的にデータを保存できます。

ただし、LocalStorageは、Cookieのようにリクエスト時にWebサーバーへデータが送信されませんので、
必要に応じて、必要なタイミングで、JavascriptによりCookieを作成する必要があります。
もちろんこのCookieの有効期限は最大7日に制限されますが、
元のデータはLocalStorageに残っていますので、何度でもCookieを再作成できます。

この方法なら、クライアントサイド（Javascript）だけで処理を完結できます。
デメリットは、Cookieを再作成するよりも前のリクエストでは、Cookieを送信できないという点です。
再来訪の初回リクエスト時にCookieが必要になる場合、この方法は適していません。

### LocalStorageの注意点

１．LocalStorageは同一オリジン単位で保存されので、Cookieのようにサブドメイン間で共有できない。
HTTPとHTTPSの違いでも別々になります。
ちなみに、少々面倒ですが、クロスドメインメッセージングと併用すると、複数のドメイン間でLocalStorageの共有が可能になります。
[クロスドメインで同じlocalStorageを使うテクニック](https://news.mynavi.jp/article/20100909-localstrage-on-many-domains/)
https://news.mynavi.jp/article/20100909-localstrage-on-many-domains/

２．LocalStorageは必ず使用できるわけではない。
問題を起こしやすいのがIE11。
LocalStorageの保存フォルダにユーザーのアクセス権がない場合、LocalStorageが使えません。
法人が使用しているPCで、たまにあります。（つい最近もありました）


## ４．何もしない
もし、該当の機能が大して重要でない場合や、
（Safariで）７日ごとにCookieが消えてもそれほど不便でないなら、
「何もしない」という判断もあり得ます。


# 最後に
いずれの方法も一長一短あります。
今までのように容易にクライアントサイドCookieを作成できなくなりました。
それは本当にCookieである必要があるか？について検討が必要かもしれません。

今後、ITPのバージョンが上がると、さらに厳しい制限が課される可能性もありますので、
Cookieの利用は最小限にするのが望ましいと思います。

掲載内容に至らない点がある場合にはコメントでご教示いただければ幸いです。