Contentfulのアセット保護機能を解説 – 画像アクセスに認証を付与してみる
どうも、ベルリンオフィスの小西です。
CMSに投稿した画像やファイルについて、公開日まで外部からのアクセスを厳密に制限しておきたいケースはありませんか?
ヘッドレスCMSのContentfulでは、画像やファイルなどのアセットの一般公開を制限し、かつ特定のユーザーもしくはトークンを付与した場合のみアクセスできるようにする Embargoed assets という機能があります。
※英語がむつかしいため、以下、本記事中では Embargoed assets を「アセット保護機能」と呼称します。
前提
1. アセット保護機能は誰でも使えるの?
先に書いておくのですが、アセット保護機能は現状Contentfulのエンタープライズ もしくは パートナーアカウント限定の機能になります。
クラスメソッドではContentfulのパートナーとして契約・技術サポートを行なっておりますので、ご興味のある方は一度ご連絡ください。
2. ContentfulにおけるアセットURLのフォーマット
Contentfulの画像含む全てのアセットは、投稿した時点では「Draft」というステータスになっていますが、この時点でエンドポイントURLが発行されています。
アセットURLフォーマット:
https://images.ctfassets.net/SPACE_ID/ASSET_ID/RANDOM_VALUE/ASSET_NAME
例:
https://images.ctfassets.net/6uruyt1jyovt/16c9dAKMoKF90j49d6mGmr/4c1acf6660d4f2573727577f97017a08/73047226-2a82-484d-8f08-7b7e1a28a2f8.JPG?h=250
ランダムな文字列が付与されるためそもそも推測が難しいものになっていますが、アクセス制限がかかっているわけではないため、一度URLがリークしてしまえばDraft状態のアセットであってもアクセスが可能になってしまいます。
この点は一般的なCMSでも同様の仕様になっているはずです。画像などをリクエストする際に公開されているエンドポイントが必要なため、仕方ない仕様かと思います。
Contentfulにおける画像を含むアセットを、URLがわかっていてもアクセスできないようにする(かつ、必要なユーザーはアクセスできるようにする)のがアセット保護機能です。
アセット保護機能の仕組み
Contentfulでアセット保護機能を有効化すると、アセット投稿時に、エンドポイントとして下記フォーマットのURLが発行されるようになります。
https://images.secure.ctfassets.net/SPACE_ID/ASSET_ID/RANDOM_VALUE/ASSET_NAME
サブドメイン部分に secure
が付与されている通り、このエンドポイントへのアクセスは認証が必要になります。
同時に、元の images.ctfassets.net
も画像を公開するまでは利用できなくなります。
上記にアクセスするとForbiddenエラーとなるように、アセットの取得には下記いずれかが必要です。
- Contentfulにログインした状態で、同じブラウザでアセットURLにアクセスする
- アセットURLのクエリパラメータに認証情報をセットする
認証は最大48時間の制限が設定でき、個別のURLごとに認証をするため、例えば一時的に外部の関係者に画像を見て欲しい時なども利用できます。
かつ、Contentfulにログインできる内部関係者は誰でも画像を閲覧することができるため、通常業務への影響を最小限に抑えることが可能です。
保護レベルの設定
全てのアセットを保護するか、非公開(Draft)のアセットのみを保護するかを選択できます。
全てのアセットの保護を選択した場合、Contentful本番用のDelivery APIでも保護されたURLが返るようになる点にご注意ください。
通常は下記のように非公開(Draft)のアセットのみを保護することが多いかと思います。
アセット保護機能の有効化方法
アセット保護機能はスペース単位での有効化となり、ダッシュボードから有効化申請が可能です。(弊社を通じてContentfulアカウントをお持ちの方は担当者までご連絡ください!)
ヘッダー[Settings] > Embargoed assets
実際にアセットを保護してみる
下記がデフォルトの状態です。アセット保護を有効化してすぐはこの状態で、保護レベルを設定するまではいきなり画像がアクセス不能になることはないのでご安心ください。
ここでは、非公開アセットのみを保護してみたいと思います。
[Asset protection level]のプルダウンから[Unpublished assets protected]を選択します。
注意: スペース単位の設定ですので、スペース内のアセット全てに適用されます。
画像をDraftステータスで投稿し、試しにシークレットブラウザで images.ctfassets.net
, images.secure.ctfassets.net
両方の画像URLにアクセスしてみると、forbiddenエラーとなります。
次に画像をPublishしたのち、images.ctfassets.net
にアクセスすると画像が正しく表示されますが、 images.secure.ctfassets.net
は引き続きforbiddenエラーとなるはずです。
上記がアセット保護機能の閲覧者側の挙動です。
Contentful APIごとの挙動はどうなる?
上述の保護レベルで[Unpublished assets protected]を選択した場合、APIごとの挙動は下記になります。
- Delivery API … 公開されているアセットは保護されないため、こちらのAPIでの挙動は何も変更がありません。
- Preview API … 保護されたアセットが返るようになります。
- Management … 保護されたアセットが返るようになります。
つまり、Preview APIにより取得したリソースを使ってサイトをビルドする場合、そのサイト内のContentfulアセットは保護されている状態 = Contentfulログインorアクセストークンがないと閲覧できない状態になります。
そのため、仮に上記のプレビューサイトに一般ユーザーがアクセスした場合、画像などは表示されない状態が想定できます。逆に関係者なら通常のサイトと同様の挙動になります。
なお、Gatsby Cloudではデフォルトでアセット保護機能からのビルドをサポートしています。
アクセストークンの生成手順
保護されたアセットにアクセスするためのトークン作成の流れは下記になります。
- アセットキーの作成 … Contentfulの ManagementAPIトークンを利用して JSON(
policy
とsecret
が含まれる)を取得 - JWT(JSON Web Token)の生成 … 1で生成した
secret
を利用してJWTを取得 - 画像URLのパラメータとして、1で生成した
policy
と2で生成したJWTを付与し、アセットにアクセスする
実際にやってみます。
1. アセットキーの作成
% curl -X POST "https://api.contentful.com/spaces/YOUR_CONTENTFUL_SPACE_ID/environments/master/asset_keys?access_token=YOUR_CONTENTFUL_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"expiresAt": 1627738900}'
YOUR_CONTENTFUL_API_TOKEN
にはContentfulダッシュボードで生成できるManagement tokenを利用します(公式ドキュメントでは Delivery API, Preview API Tokenでも可能とのことでしたが、試したところうまくいきませんでした)。
expiresAt
にはトークンに設定したい有効期限をUnix Timestampで入力します(最大48時間後)。
https://www.unixtimestamp.com/index.php とかで簡単にわかります。
上記の返り値は下記のようなフォーマットになります。
{"policy":"eyJhbGdiOiJIUzI1NiRsInR5cCI7IfpXVCIsImtpZCI6IjE6MSJ9.eyJleHAiOjE2MjY3ODR1MDAsInN1YiI7ImF3aHB3YWo2d3NxcSIsImF1ZCI6ImFkbiIsImp5aSI6IjQ0MmM3NWQ5LTA3YmYtNDhiMi1hMjBjLWE5MTIxYjM4TjhjYSIsImN0Zjp1bnB134wI6dHJ1ZX0","secret":"XXXXXX"}%
なお、アセットキーはキャッシュして次回生成まで使い回すことが推奨されています。
2. JWT(JSON Web Token)の生成
CLIでJWTを生成するために https://github.com/mike-engel/jwt-cli を使います(JWTが生成できればここはなんでもいいです。)。
% brew install mike-engel/jwt-cli/jwt-cli
JWTのリクエストを行います。
% jwt encode -P 'sub=https://images.secure.ctfassets.net/{{リクエストしたい画像パス}}' --secret "{{手順1で取得したsecretの値}}"
返り値例:
eyJ0eXAiOEJKV1QiLCJhbGciO5JIUzI1NiJ9.eRJpYXQiOjE1NjE0MjY3MzIsInN1YiI6Imh0dHBzOi8vaW1hZ2VzLnNlY3VyZS5jdGZhc3NldHMubmV0L1F3aHB5YWo2d4NxcS8xald4ZEh5dGpGWVlJUkhWc0tPUTZuLzQzOTE5NDhkMDYwYWZkxjQ2MTkwMTY3NjZjZmQ3NzgwL2tlEzMuanBAZyJ9.a9JwEzr8MCmgnpeRGSt4YCW0THdKZ9QfhKD0lJqhT_U
なおオプションとして、JWT生成時のペイロードで exp=1627738900
としてUNIXタイムスタンプを渡すことで、鍵の有効期限を上書きすることも可能です。上述の通りアセットキーを使いまわせるため、ここで個別の期限を設定できる便利なオプションです。
3. 画像にアクセス
手順1, 2で得られたトークンをパラメータとして付与します(長い!)。
URL例(末尾に ?token=...&policy=...
が付与されています):
https://images.ctfassets.net/6uruyt1jyovt/16c9dAKMoKF90j49d6mGmr/4c1acf6660d4f2573727577f97017a08/73047226-2a82-484d-8f08-7b7e1a28a2f8.JPG?token=eyJ0eXAiOEJKV1QiLCJhbGciO5JIUzI1NiJ9.eRJpYXQiOjE1NjE0MjY3MzIsInN1YiI6Imh0dHBzOi8vaW1hZ2VzLnNlY3VyZS5jdGZhc3NldHMubmV0L1F3aHB5YWo2d4NxcS8xald4ZEh5dGpGWVlJUkhWc0tPUTZuLzQzOTE5NDhkMDYwYWZkxjQ2MTkwMTY3NjZjZmQ3NzgwL2tlEzMuanBAZyJ9.a9JwEzr8MCmgnpeRGSt4YCW0THdKZ9QfhKD0lJqhT_U&policy=eyJhbGdiOiJIUzI1NiRsInR5cCI7IfpXVCIsImtpZCI6IjE6MSJ9.eyJleHAiOjE2MjY3ODR1MDAsInN1YiI7ImF3aHB3YWo2d3NxcSIsImF1ZCI6ImFkbiIsImp5aSI6IjQ0MmM3NWQ5LTA3YmYtNDhiMi1hMjBjLWE5MTIxYjM4TjhjYSIsImN0Zjp1bnB134wI6dHJ1ZX0
上記にアクセスしてみると、画像が正しく表示されました!
最後に
今回はContentfulのアセット保護機能の概要、使い方について解説してみました。
ユースケースとしては有料コンテンツの配信や個人情報の配布などが思いつきますし、他にも例えば「決算発表まで特定のファイルは絶対公開できないし、万が一にもURLを推測されてしまうとまずい。しかし内部確認のためにあらかじめ投稿しておきたい」というようなケースで非常に有用な機能かと思います。
クラスメソッドはContentfulのパートナーとして導入のお手伝いをさせていただいています。