AWS SDK for Javaを使ってカスタムポリシーを使用した署名付きCookie（CloudFront用）を設定する

CloudFront配下のコンテンツへのアクセスを制限する場合、

 * 規定ポリシーを使用した署名付きURL
 * カスタムポリシーを使用した署名付きURL
 * 規定ポリシーを使用した署名付きCookie
 * カスタムポリシーを使用した署名付きCookie

が使えます。

【参考：AWS内ドキュメントへのリンク】
**[署名付き URL の使用](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html)**
**・[既定ポリシーを使用して署名付き URL を作成する](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html)**
**・[カスタムポリシーを使用して署名付き URL を作成する](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html)**
**[署名付き Cookie の使用](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html)**
**・[既定ポリシーを使用した署名付き Cookie の設定](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-canned-policy.html)**
**・[カスタムポリシーを使用した署名付き Cookie の設定](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html)**

が、

 * 署名付きURLを使用する場合は、アクセスするリソースのそれぞれのリンクに署名を付ける必要がある
 * 規定ポリシーを使用する場合は、アクセス許可対象の指定にワイルドカード（**「?」「*」**）が使えない
 
ということで、

 * 1ファイル毎にアクセス権を与える
 * アプリケーション側から1個ずつリソースへのリンクを動的生成する

という場合には、規定ポリシーや署名付きURLで良いのですが、

 * アクセス制限対象のコンテンツがHTMLファイル＋.css＋画像＋動画で構成される

という場合には、署名付きURLでは読み込む.css、画像、動画の各ファイルへのリンクにも署名を付けないといけなくなって現実的でないので、今回は**カスタムポリシーを使用した署名付きCookie**を使って実験してみました。

# 0. 今回の実験内容

 * アプリケーションで署名付きCookieを発行し、ブラウザにセットします。
 * アクセス制限対象のコンテンツは、S3バケット**「testmatsusignedcookie」**に保存します。
 * アプリケーションは**「https://hmatsu47.site/ 」**で公開します（Cookieのセットは**「/set-cookie」**で実行）。
 * アクセス制限対象のコンテンツは、**「https://www.hmatsu47.site/ 」**で公開します（CloudFrontディストリビューション）。
 * サードパーティーCookieの発行方法や受け入れ制限で困らないよう、発行する署名付きCookieにはDomain属性**「hmatsu47.site」**を指定します。
 * あわせて、Secure属性（非SSL/TLSで公開する場合は無効にする）、HttpOnly属性（JavaScriptでCookieを処理する場合は無効にする）も有効にします。

# 1. CloudFront＋S3側の設定

以下の流れで設定します。

## 1-1. AWS マネジメントコンソールでCloudFrontキーペアを作成する

以下、IAMユーザでは作成できないので、**ルート認証情報を持つユーザ**を使って操作を行います。

![01_sec_auth.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/7b812b5f-cdaf-338f-3840-63195d457a11.jpeg)
①右上のユーザのメニューから、**「セキュリティ認証情報」**を選択。
②**「CloudFrontのキーペア」**を展開
③**［新しいキーペアの作成］**をクリック
④**［プライベートキーファイルのダウンロード］**をクリックし、プライベートキーファイルをダウンロード
⑤ポップアップを閉じた画面で、キーペアIDを確認・記録
　※ダウンロードしたプライベートキーファイルの**「pk-」**と**「.pem」**の間の文字列からキーペアIDを確認することもできます。
⑥以下のリンク先を参考に、OpenSSLでプライベートキーファイル（.pem形式）を.der形式にコンバート
　**[CloudFront プライベートキーの形式を変更する（.NET および Java のみ）](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-reformatting-private-key)**
 ※Windows環境用のOpenSSLのバイナリ（.exe）を提供しているサイトがありますので、それを使うことも可能です。

```text:opensslコマンド
openssl pkcs8 -topk8 -nocrypt -in 【プライベートキーファイル名】.pem -inform PEM -out 【出力ファイル名】.der -outform DER
```

## 1-2. CloudFrontディストリビューションを作成する

![02_cf_create_dist_01.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/23941d5e-a8dd-e00b-df97-dd8ccfed375f.jpeg)
①コンソールのCloudFront→Distributionで、**［Create Distribution］**をクリック

![02_cf_create_dist_02.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/10afbd12-c1b1-1844-bd46-5d92238a8d66.jpeg)
②Webの**［Get Started］**をクリック

![02_cf_create_dist_03.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/d5d785af-9d39-3a04-b0c6-f4e46258eeae.jpeg)
③オリジンのS3バケットに関する項目を設定
④**「Restrict Bucket Access」**で**「Yes」**を選択
⑤**「Origin Access Identity」**で**「Create a New Identity」**を選択
⑥**「Grant Read Permissions on Bucket」**で**「Yes, …」**を選択

![02_cf_create_dist_04.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/ac96bb4b-b791-504f-60ba-4367ab4f23f4.jpeg)
⑦**「Restrict Viewer Access」**で**「Yes」**を選択
⑧**「Trusted Signers」**で**「Self」**にチェック
⑨**「Alternate Domain Names」**にドメイン名（CloudFrontのディストリビューションに割り当てるFQDN）を指定
　※ディストリビューション作成完了後、Route 53の当該レコードでAレコードのエイリアスを指定します。

![02_cf_create_dist_05.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/3cb30957-4fee-d784-d580-a42102f9913b.jpeg)
⑩SSL/TLS関連項目を設定（**「Custom SSL Certificate」**を選択後、使用する証明書を選択するか、新規SNI SSL証明書をリクエストする）

![02_cf_create_dist_06.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/27929d27-aca2-dea5-f590-8f613599c9ca.jpeg)
⑪**「Create Distribution」**をクリック

![02_cf_oai.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/35721e38-3aab-73a3-f7c7-6214d4a8dea5.jpeg)
⑫**「Origin Access Identity」**をクリック
⑬**「Amazon S3 Canonical User ID」**を確認
⑭ディストリビューションが作成されるのを待つ

## 1-3. S3バケットのアクセス権限を設定する

①コンソールのS3で、公開対象のS3バケット→**「アクセス権限」タブ**を選択

![03_s3_acl.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/fedfe897-686b-518d-1100-028655ec1f71.jpeg)
②**［アクセスコントロールリスト］**をクリック
③**［ユーザを追加する］**をクリック
④CloudFront設定の⑬で確認した文字列を入力
⑤**「オブジェクトアクセス」**で**「読み込み」**にチェック
⑥**［保存］**をクリック

![03_s3_policy.jpg](https://qiita-image-store.s3.amazonaws.com/0/123899/88081b14-7dc1-5954-5ee1-01e748a565a4.jpeg)
⑦**［バケットポリシー］**をクリックし、（CloudFrontディストリビューション作成時の指定により）ポリシーが設定されていることを確認

# 2. アプリケーションに署名付きCookieをセットするコードを実装する

今回はローカル開発環境での実験のため、Spring Boot環境で実装しました。
以下、**「/set-cookie」用Controllerクラス**についてのみ記します。

あらかじめ、.der形式に変換しておいたプライベートキーファイルを、**「src/main/resources」**の下に**「files」**フォルダを作成してその中に**「private-key.der」**という名前で保存しておきます。
今回はSpring Bootの環境に合わせてこの場所にファイルを保存したため、プライベートキーファイルの内容の取り出し方がやや面倒な形になっていますが、通常のServlet環境でWEB-INF内にファイルを保存しておけば、もっと素直に取り出せるはずです。

なお、Amazonでは、キーペア（プライベートキーとパブリックキー）を90日毎に更新することを推奨しており、そのような短いサイクルでの更新を行うのであれば、キーペアIDとプライベートキーファイルは、ハードコーディングしたりアプリケーションのパッケージに固定的に含めたりするよりも、アプリケーションと分けて更新できる形で保持するのがいいでしょう。

```java:SetCookieController.java
package site.hmatsu47.springboot;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;

import com.amazonaws.services.cloudfront.CloudFrontCookieSigner;
import com.amazonaws.services.cloudfront.CloudFrontCookieSigner.CookiesForCustomPolicy;
import com.amazonaws.util.DateUtils;

import java.io.File;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Controller;

@Controller
public class SetCookieController {

	@Autowired
    ResourceLoader resourceLoader;
	
	@RequestMapping("/set-cookie")
	public void setCookie(HttpServletResponse res) throws Exception{
		String url = "https://www.hmatsu47.site/";		// URL
		String filepath = "files/private-key.der";		// プライベートキー
		Resource resource = resourceLoader.getResource("classpath:" + filepath);
		File file = resource.getFile();
		byte[] privateKeyByteArray = Files.readAllBytes(file.toPath());
		PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyByteArray);
		KeyFactory kf = KeyFactory.getInstance("RSA");
		RSAPrivateKey privateKey = (RSAPrivateKey) kf.generatePrivate(keySpec);
		String resourcePath = "*";						// アクセスを許可する範囲
		String keyPairId = "【キーペアID】";					// キーペアID
		Date activeFrom = null;							// 有効期間：開始（null＝即時）
		Date expiresOn = DateUtils.parseISO8601Date("2020-12-31T23:59:59.999Z"); // 有効期間：終了（UTCで）
		String ipRange = "0.0.0.0/0";					// アクセス元のIPアドレス範囲
		// cookie用のキー／値の生成
		CookiesForCustomPolicy cookies = CloudFrontCookieSigner.getCookiesForCustomPolicy(
				url + resourcePath, privateKey, keyPairId, expiresOn, activeFrom, ipRange);
		// cookieの設定
		Cookie cookiePolicy = new Cookie(
				cookies.getPolicy().getKey(), cookies.getPolicy().getValue());
		cookiePolicy.setDomain("hmatsu47.site");
		cookiePolicy.setHttpOnly(true);
		cookiePolicy.setSecure(true);
		res.addCookie(cookiePolicy);

		Cookie cookieSignature = new Cookie(
				cookies.getSignature().getKey(), cookies.getSignature().getValue());
		cookieSignature.setDomain("hmatsu47.site");
		cookieSignature.setHttpOnly(true);
		cookieSignature.setSecure(true);
		res.addCookie(cookieSignature);

		Cookie cookieKeyPairId = new Cookie(
				cookies.getKeyPairId().getKey(), cookies.getKeyPairId().getValue());
		cookieKeyPairId.setDomain("hmatsu47.site");
		cookieKeyPairId.setHttpOnly(true);
		cookieKeyPairId.setSecure(true);
		res.addCookie(cookieKeyPairId);
		// リダイレクトで遷移
		res.sendRedirect(url + "index.html");
	}
}
```

# 3. 実行する

公開対象のS3バケットには、適当な内容のindex.htmlと、index.htmlの指定で読み込まれる画像などを保存しておきます。

まず、ブラウザを起動し、**「https://www.hmatsu47.site/index.html 」**を開きます。すると、アクセス権限がないので対象のページが開かず、エラーになります。

次に、2. で実装した**「https://hmatsu47.site/set-cookie 」**を開きます。すると、署名付きCookieがドメイン**「hmatsu47.site」**およびそのサブドメインの範囲を対象にセットされた後、リダイレクトされ、今度は正しくページが開きます。

ブラウザを閉じると、署名付きCookieが消えるため、再度ブラウザを起動したときには**「https://www.hmatsu47.site/index.html 」**は開かず、エラーになります。

※今回は、取り急ぎ確認するために、ローカル開発環境（Windows 8.1）でSpring Bootアプリケーションを実行しました（hostsファイルを編集し、**「hmatsu47.site」**をローカルに向ける。自己認証キーを使ってSpring BootでSSL/TLS通信できる状態に設定（方法についてはQiitaにも過去に複数の投稿あり）。待ち受けポートは8443などではなく443にする）。

```text:hostsファイル（追記）
127.0.0.1	hmatsu47.site
```

今回用意した開発環境（Windows 8.1上のSTS 3.8.4.RELEASE）ではこれでうまく動きましたが、他の環境（ローカル環境）で動く保証はないので、うまく動かないときは通常の公開サーバ環境で試してみたほうがよさそうです。

# 4. 動画再生への利用

以下の記事を参照してください。
**[CloudFront配下のアクセス制限付き動画（HLS形式）を、署名付きCookieを使ってVideo.jsで再生する](http://qiita.com/hmatsu47/items/dde84a8d059ea2eb62dc)**