14
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Node.jsでAmazon CloudFrontの署名付きURLを生成する

Last updated at Posted at 2018-10-25

やりたいこと

  • 認証が必要なWebアプリケーションをAWS上に構築する
    • Amazon S3で画像ファイルを管理する
    • 画像ファイルには認証済みユーザーのみがアクセス可能
    • 独自URLで公開する
    • 当然SSL化する

いろいろとググったところ、Amazon CloudFront署名付きURL (signed url) を使うと目的を達成できそう。
環境整備が面倒くさかったので手順を整理しておきます。

この記事では 署名付きURL の作成方法について取り上げます。Webアプリケーションの構築については範囲外です

手順

リソース管理用のS3バケットを準備する

画像ファイルを格納するS3のバケットを作成します。
今回は cloudfront-private-content という名前にしました。

テストのために cat.jpgdog.jpg をバケットにアップロードします。

CloudFrontでS3バケットを公開設定する

CloudFrontを迂回してアクセスできないように設定します。
Step 2: Create distribution の画面で以下の設定を行います。

Origin Settings

項目 設定値 説明
Origin Domain Name 作成したS3バケットを指定 バケットのWebサイトホスティング設定は不要
Restrict Bucket Access Yes バケットへのアクセスをCloudFront経由に制限する
Origin Access Identity Create a New Identity バケットアクセスのための認証アカウントを作成
Grant Read Permissions on Bucket Yes, Update Bucket Policy バケットのアクセス権限を変更し、CloudFrontによる読み取り権限を付与する

Default Cache Behavior Settings

項目 設定値 説明
Viewer Protocol Policy Redirect HTTP to HTTPS HTTPアクセスされたらHTTPSにリダイレクト
Restrict Viewer Access (Use Signed URLs or Signed Cookies) Yes 署名付きURLあるいはCookieによるアクセスのみに制限する

コンソールの CloudFront Distributions にて status が In Progress から Deployed に変わるまで結構時間がかかります。
ゆっくりコーヒーを飲む時間くらいはかかるので、余裕を見て作業しましょう。

2018-11-12追記

公開当初、この構成のキモである Restrict Viewer Access の設定が漏れてました。
今日、自分でこの記事を見て、「なんでこの設定で署名付きURL以外のアクセスが制限されるんだ?」って悩んでしまいました...

CloudFrontへ独自ドメインでアクセスする

Route 53 で先程作成した CloudFront Distribution へのaliasを作成します。

まず、CloudFrontのコンソールで Domain Name をコピーしておきます。
xxxxxxxxxxxxx.cloudfront.net

つづいて、Route53で Aレコード を追加します。

項目 設定値
Name private-content.example.com
Type A - IPv4 address
Alias Yes
Alias Target xxxxxxxxxxxxx.cloudfront.net

ふたたび CloudFrontのコンソールに戻り、Alternate Domain Names (CNAMEs) を更新します。

リストから作成したDistributionを選択し、Distribution Settingsボタンをクリックします。
GeneralタブのEditボタンをクリックします。
Alternate Domain Names (CNAMEs) にRoute53で登録したAレコードのNameを入力します。
Yes, Editボタンをクリックし、変更を保存します。

独自ドメインをSSL化する

また Distribution Settings > General > Editボタンをクリックします。
SSL CertificateCustom SSL Certificate (example.com): を選択したいところですが、まだアクティブになっていないと思います。(選択可能な証明書があればアクティブになる)
AWS Certificate Manager で証明書を作成し登録します。

Request or Import a Certificate with ACMボタンをクリックします。
AWS Certificate Managerが開きます。このときリージョンが バージニア北部 となっていることを確認してください。(そうでなければ変更する)
ドメイン名は *.example.com として続行します。
今回、ドメインの管理もRoute53で行っているのでCNAMEの登録による確認で証明書が発行されます。

ここでも検証が完了するまで少し時間がかかりますが、状況発行済み になるまで何度かリロードしてください。

(私の環境では、以降のCloudFrontのコンソールでなかなか証明書が一覧に表示されず、しばらく悩みました。
経験上、AWSコンソールでの作業はひとつづつ確実にこなすのが良いです。)

CloudFrontのコンソールに戻り、SSL CertificateCustom SSL Certificate (example.com): が選択可能であればチェックしてください。
(私の環境では一旦Editをキャンセルし、ブラウザのリロードを行い、再度Edit画面に戻ると選択可能になっていました。)

Custom SSL Certificate (example.com): のテキストボックスにフォーカスを合わせると、AWS Certificate Managerで発行した証明書が表示されますので、選択してください。

Yes, Editボタンをクリックし、変更を保存します。

キーペアの作成

署名付きURLを生成するための公開鍵、秘密鍵、アクセスキーIDを取得します。

AWSコンソールのヘッダー右上、ユーザー名の部分をクリックして セキュリティ認証情報 を選択します。
ダイアログが表示される場合はContinue to Security Credentialsボタンをクリックして続行します。

CloudFrontのキーペア にて 新しいキーペアの作成 をクリックし、公開鍵と秘密鍵をダウンロードします。
また、CloudFrontのキーペアアクセスキーID が表示されるので、これもコピーしておきます。

Node.jsでAmazon CloudFrontの署名付きURLを生成する

やっと本題。
今回は署名付きURLを生成する機能だけを実装します。

適当にNode.jsのプロジェクトフォルダを作成し、AWS-SDKとその他のパッケージをインストールします。
(その他のパッケージは署名付きURLの生成に直接関与するものではありません。AWS-SDKだけが必須です。)

$ mkdir cloudfront-secret-content
$ cd cloudfront-secret-content
$ npm init -y
$ npm install --save aws-sdk dotenv fs-extra moment

以下のように、コンソールに生成したURLを出力するコードを作成します。

index.js
require('dotenv').config();
const AWS = require('aws-sdk');
const fs = require('fs-extra');
const moment = require('moment');

// 対象となるリソースのURL
const target = 'https://private-content.example.com/cat.jpg';

/**
 * AWS.CloudFront.Signer.getSignedUrlを呼び出す。
 *
 * Class: AWS.CloudFront.Signer — AWS SDK for JavaScript
 * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront/Signer.html
 * @param {String} keypairId
 * @param {String} privateKey
 * @param {Object} options 
 */
function getSignedUrlAsync(keypairId, privateKey, options) {
  return new Promise((resolve, reject) => {
    // Signerインスタンスを生成
    const signer = new AWS.CloudFront.Signer(keypairId, privateKey);
    // URL生成
    signer.getSignedUrl(options, (err, url) => {
      if (err) {
        reject(err);
      }
      resolve(url);
    });
  });
}

async function main() {
  // private keyを読み込む
  const privateKey = await fs.readFile(process.env.PRIVATE_KEY_FILE, { encoding: 'utf-8' });

  // 期限を設定
  // 現在日時から1日後まで有効とする
  const expires = moment.utc().add(1, 'days').unix();

  // URL生成
  const url = await getSignedUrlAsync(
    // キーペアのID
    // AWSコンソールの以下の場所で確認可能
    // セキュリティ認証情報 > CloudFront のキーペア > アクセスキーID
    process.env.KEYPAIR_ID,
    // 秘密鍵を渡す
    privateKey,
    {
      // 対象となるCloudFrontのURL
      url: target,
      // 生成されるURLの期限 (UTCのunixtime)
      expires
    }
  );
  
  console.log(url);
}

main().then(() => {
  console.log('done.');
}).catch((err) => {
  console.error(err);
});

秘密鍵の保存場所とアクセスキーIDを .env ファイルに記述します。

.env
KEYPAIR_ID=xxxxxxxxxxxxxxxxxxxx
PRIVATE_KEY_FILE=./secret/pk-xxxxxxxxxxxxxxxxxxxx.pem

実行すると以下のような結果が返ります。

$ node index.js
https://private-content.example.com/cat.jpg?Expires=1540535260&Key-Pair-Id=XXXXXXXXXXXXXXXXXXXX&Signature=.....
done.

返されたURLにブラウザからアクセスし、画像が表示されることを確認してください。
また、QueryString部分をカットして 403 が返されること、cat.jpgdog.jpg に変更してもアクセスできないことを確認します。


参考

14
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?