CloudFront配下のアクセス制限付き動画（HLS形式）を、署名付きCookieを使ってVideo.jsで再生する

相変わらずタイトルが長いですが（Googleで検索するとタイトルの途中でぶった切られてます…と思ったら誰かが登録したはてブでした）、
**[AWS SDK for Javaを使ってカスタムポリシーを使用した署名付きCookie（CloudFront用）を設定する](http://qiita.com/hmatsu47/items/8edd5cfde92bfd0d5a6c)**
で設定した環境に、HLS形式の動画（.m3u8ファイル＋複数に分割された.tsファイル）を配置して、外部のアプリケーションから読み込ませるものです。

なお、Video.jsでのHLS形式の動画再生については、（Video.jsのバージョンが少し古いですが）以下の記事が参考になります。
**[Video.js を使って HLS形式の動画をストリーミング再生する（akiyoko blog）](http://akiyoko.hatenablog.jp/entry/2015/08/11/015852)**

**6/5追記：CloudFrontのBehavior設定変更手順を書き漏らしていたので修正しました。**

# 0. 前提

まず、
**[AWS SDK for Javaを使ってカスタムポリシーを使用した署名付きCookie（CloudFront用）を設定する](http://qiita.com/hmatsu47/items/8edd5cfde92bfd0d5a6c)**
の通りに、CloudFrontとS3が設定されているものとします。
また、テスト用のアプリケーションはローカル開発環境にSpring Bootを使って実装するものとします（要hostsファイル変更）。

アプリケーションやコンテンツを公開するドメインは、↑の記事に合わせます。
**アプリケーション : https://hmatsu47.site/**
→**「https://hmatsu47.site/set-cookie 」**にアクセスすると**「https://hmatsu47.site/index.html 」**に遷移し、動画プレイヤーが表示されます。
**動画コンテンツ　 : https://www.hmatsu47.site/**
→S3バケット**「testmatsusignedcookie」**直下に**「sample.m3u8」**ファイルと、同ファイルに記述した分割動画ファイル（**XXXXX.ts**）を保存します。

いずれも、自身の環境に合わせて読み替え／書き替えてください。

# 1. CloudFrontのBehaviorの設定を変更する

CloudFrontで、対象ディストリビューションのBehavior設定を変更し、S3バケットにフォワードされるHTTPリクエストで、要求ヘッダ中の**「Origin」**が除外されないようにして保存します。
![cf_whitelist.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/e5f9d72a-66d9-50f6-c601-d25e776bb7fe.jpeg)
※上図ではHTTPメソッドとして「GET」「HEAD」に加えて「OPTIONS」を受け入れるよう設定していますが、preflight requestが飛ぶ場合などでうまくいかないときは、「OPTIONS」メソッドの受け入れも試してみてください。

# 2. S3バケット設定（CORS）を変更する

S3バケットのアクセス権限で、CORS構成エディターを使って設定を変更し、クロスオリジンのScriptアクセスを許可します。
※先の記事の**「[1-3. S3バケットのアクセス権限を設定する](http://qiita.com/hmatsu47/items/8edd5cfde92bfd0d5a6c#1-3-s3%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E6%A8%A9%E9%99%90%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)」**⑥・⑦の画面にある**[CORS の設定]**をクリックし、構成エディタで内容を変更して保存します。

```text:CORS設定
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>http://hmatsu47.site</AllowedOrigin>
    <AllowedOrigin>https://hmatsu47.site</AllowedOrigin>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <MaxAgeSeconds>3600</MaxAgeSeconds>
    <ExposeHeader>ETag</ExposeHeader>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```

※*\<MaxAgeSeconds\>*には適切な有効期間を指定します。

# 3. アプリケーションを変更する

先の記事の**「[2. アプリケーションに署名付きCookieをセットするコードを実装する](http://qiita.com/hmatsu47/items/8edd5cfde92bfd0d5a6c#2-%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E7%BD%B2%E5%90%8D%E4%BB%98%E3%81%8Dcookie%E3%82%92%E3%82%BB%E3%83%83%E3%83%88%E3%81%99%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B)」**の**「SetCookieController.java」**末尾の**コメント行（「*// リダイレクトで遷移*」）以降**を変更して、P3Pコンパクトポリシーを応答ヘッダに付与するとともに（IE11用）、ローカル側のindex.htmlに遷移する形に書き換えます。

```java:SetCookieController.java（変更部分＝末尾のみ）
		res.setHeader("P3P", "CP=\"【P3Pコンパクトポリシー】\"");
		// リダイレクトで遷移
		res.sendRedirect("index.html");
	}
}
```

P3Pコンパクトポリシーは、IE11で別オリジン（別FQDN or 別プロトコル／ポート番号のサイト）にScriptからCookieを送信できるようにするために指定します。
正しい内容でなくても任意の文字列がセットされていれば動作してしまうようです。
なお、IE以外のブラウザでは、すでに機能していないため、指定がなくても別オリジンへの（Scriptによる）Cookie送信は可能です。

# 4. 動画再生に必要なファイルを配置する

Video.jsおよび必要な.jsファイル等と、それらを呼び出すindex.htmlを用意します。

以下、Spring Boot環境では、**「src/main/resources」**の下に**「static」**フォルダを作成して配置します。Video.js関連のファイルはダウンロードするなどして用意してください。

 * video-js.5.19.2 （フォルダ）
 * video-js.5.19.2/video-js.min.css
 * video-js.5.19.2/video.min.js
 * video-js.5.19.2/videojs-contrib-media-sources.min.js
 * video-js.5.19.2/videojs-contrib-hls.min.js
 * video-js.5.19.2/video-js.swf
 * index.html 

```html:index.html
<html>
  <head>
    <title>HLS test</title>
    <link href="/video-js.5.19.2/video-js.min.css" rel="stylesheet">
    <script src="/video-js.5.19.2/video.min.js"></script>
    <script src="/video-js.5.19.2/videojs-contrib-media-sources.min.js"></script>
    <script src="/video-js.5.19.2/videojs-contrib-hls.min.js"></script>
    <script>
      videojs.options.flash.swf = "/video-js.5.19.2/video-js.swf"
    </script>
  </head>
  <body>
    <video id="test" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="【動画プレイヤー横幅】" height="【同・高さ】">
    </video>
    <script>
      var player = videojs('test', {techOrder: ['flash', 'html5']});

      player.src({
        src: 'https://www.hmatsu47.site/sample.m3u8',
        type: 'application/x-mpegURL',
        withCredentials: true
      });
    </script>    
  </body>
</html>
```

署名付きCookieをScriptからCloudFrontに送信するために、**「withCredentials」**を**「true」**にしています。

IE11ではVideo.js＋HTML5によるHLS形式の動画再生に不具合があるため、Flashを使った再生となります。
因みに、ローカルの「video-js.swf」を読み込む指定をしていますが、IE11だけこの指定は無視され、CDNからダウンロードされてしまいます…。結果として、IE以外のブラウザで外部のファイルをわざわざ読みにいかないようにするためだけにこの指定がある、ということになってしまっています。

# 5. 動画ファイルをS3バケットにアップロードする

「sample.m3u8」ファイルおよび分割動画ファイルについても、S3バケットにアップロードしておきます。

# 6. 動画再生をテストする

以上の作業で、署名付きCookieで保護された動画を再生することができる環境ができあがります。

ローカルのブラウザで、**「https://hmatsu47.site/set-cookie 」**を開くと動画再生画面が表示され、再生ボタンをクリックすると動画の再生が始まります。

※index.htmlから読み込むファイルとコンテンツタイプの指定を適切に変更することで、HLS形式でない動画を再生することも可能です。

なお、IE11でFlashを使う再生の場合、HTML5で再生するよりも分割動画の読み込みタイミングが遅いため、再生機器や回線の速度によっては動画がギクシャクしやすくなります。ご注意ください。

# 7. 補足／CookieとCORSについて

ややこしいのですが、Cookieの有効範囲とCORSによる「オリジン間のアクセス制御」は別物です。

**■Cookieの有効範囲：Domain属性、Secure属性、Path属性、HttpOnly属性で指定**

 * Domain属性を指定しなければSet-Cookieしたサーバと同じFQDN、指定していれば指定ドメインおよびそのサブドメイン。今回のケースでは**「hmatsu47.site」**を指定しているため、**「hmatsu47.site」**および**「www.hmatsu47.site」**のどちらでも有効（テストには使っていないが**「app.hmatsu47.site」**、**「video.hmatsu47.site」**などでも有効）。なお、Set-Cookieしたサーバと関係のないドメインをDomain属性で指定しても無効（ブラウザで破棄される）。
 * Secure属性を指定しなければHTTP／HTTPSの両方で、指定すればHTTPSのみで有効。ポート番号の区別なし。
 * Path属性を指定すれば指定のパス以下で有効（Path属性には色々問題はあるが、説明は省略）。
 * HttpOnly属性を指定すればScriptから利用不可。
 * サードパーティーCookie（ブラウザが読み込んだHTML本体とは別のドメインのサーバから画像や.jsなどを読み込むことによりセットされるCookie）については、ブラウザの設定によって有効／無効が決まる。

**■CORS：オリジン（FQDNとプロトコル・ポート番号の組み合わせ）をまたぐアクセスを制御**

 * 同一オリジンにあたるのは、**「FQDNとプロトコル（HTTP／HTTPS）およびポート番号（80／81、443／8443など）の組み合わせが同じ」**。つまり、**「https://hmatsu47.site 」**と**「https://www.hmatsu47.site 」**は別オリジン。**「http://www.hmatsu47.site 」**と**「https://www.hmatsu47.site 」**、**「http://hmatsu47.site 」**と**「http://hmatsu47.site:81 」**も別オリジン。
 * （ブラウザが処理中の）別オリジンのサーバのScriptからアクセスを受けるようなケースで、アクセス制御に使用される。**「https://hmatsu47.site からの読み込みアクセスは、リクエストを受けてから○○秒の範囲で有効にするよ」**というようなイメージ。アクセスを受けたサーバがアクセス元オリジンを区別できるように、ブラウザからのリクエストの要求ヘッダにはオリジンの情報（Origin）を付与する。
 * デフォルトでは、たとえブラウザが（アクセス先の別ドメインに対して有効な）Cookieを持っていたとしても、Scriptからのリクエストでは送信しない。JavaScriptでは、**XMLHttpRequestでwithCredentials属性を有効にする**ことで（リクエストを送信するとき一緒に）送信される。なお、CORSとは別に、IE11の場合はP3Pコンパクトポリシーも必要になる。
 * Cookieを受け付けるかどうかは、リクエストを受けるサーバのCORSでも指定できる。

つまり、

 * いくらwithCredentials属性を有効にしてXMLHttpRequestでリクエストを送信しても、相手先のドメインで有効なCookieを持っていないとCookieは送信されない。
 * サードパーティーCookieがブラウザに受け入れられるかどうかとCORSの設定は別問題であるため、分けて考える必要がある。

ということです。

なお、今回のサンプルでは、Javaのコードで署名付きCookieを生成する際に「HttpOnly」属性を付けています。
これは、アクセス元のJavaScriptの中ではこのCookieを参照する必要がないからです。結果、「HttpOnly」属性を付けていても問題なく動作します。