React アプリで Amazon Cognito 認証済みユーザーにのみ Amazon S3 静的コンテンツへのアクセスを許可したい -UI編-

こんにちは、広野です。

これまでいろいろと React アプリを AWS リソースと連携させるための仕組みをブログ記事にしてきましたが、絶対に使うであろうユースケースを書いていませんでした。以下、順を追って説明したいと思います。

本記事は UI 編です。

前回の記事

こちらをご覧になった上で、本記事にお戻りください。

アーキテクチャ編では、背景やアーキテクチャを説明しています。

 

環境編では、主に Lambda@Edge 関数や CloudFront による環境づくりの説明をしています。

 

React アプリ側の実装

以下の Amazon CloudFront にリクエストを投げる React アプリ側のコードを説明します。

リクエストにはパターンがある

リクエストにはいろいろなパターンがあります。

  • 画像の取得。<img>タグが例。
  • PDF などのファイルへのリンク。<a>タグが例。
  • 動画のストリーミング。
  • HTTP GET メソッドによるデータの取得。

当然それぞれのコードは異なるのと、いずれもデフォルトでは Authorization ヘッダー埋め込みに対応していないので、ひとつひとつに対してコードをカスタマイズする必要があります。

しかし、とにかくやることはリクエストの Authorization ヘッダーにトークンを埋め込むことです。それさえできればよいのです。

 

リクエストパターンごとのコード紹介

ということで、リクエストのパターンごとに実際のコード例を紹介します。

共通の設計にしたことが、最新のトークン取得です。AWS Amplify のモジュールを使用して、Amazon Cognito に問い合わせします。古いトークンを使用してしまうことを避けるためリクエスト直前に都度トークンを取得する設計にしています。

 

画像

<img> タグをカスタマイズします。<AuthImg> というカスタムコンポーネントを作成し、<img> の代わりに使用します。

画像のバイナリデータを取得し、仮の URL が作られ、それを <img> に埋め込む動きをしています。

  • カスタムコンポーネント
import { useState, useEffect } from 'react';
import { fetchAuthSession } from 'aws-amplify/auth';

//Cognito認証付きS3へのカスタムimgコンポーネント
const AuthImg = ({ src, alt, className, ...props }) => {
  const [imgSrc, setImgSrc] = useState("");
  useEffect(() => {
    let objectUrl = "";
    const fetchImage = async () => {
      try {
        const { tokens } = await fetchAuthSession();
        const idToken = tokens.idToken.toString();
        const response = await fetch(src, {
          method: 'GET',
          headers: {
            'Authorization': `Bearer ${idToken}`
          }
        });
        if (!response.ok) throw new Error('Image fetch failed');
        const blob = await response.blob();
        objectUrl = URL.createObjectURL(blob);
        setImgSrc(objectUrl);
      } catch (error) {
        console.error("Failed to load authenticated image:", error);
      }
    };
    if (src) {
      fetchImage();
    }
    // クリーンアップ関数:コンポーネントが消える時にメモリを解放
    return () => {
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    };
  }, [src]);
  // 読み込み中は何も出さない、またはプレースホルダーを出す
  if (!imgSrc) return <div className={className} style={{ backgroundColor: '#eee' }} />;
  return <img src={imgSrc} alt={alt} className={className} {...props} />;
};
  • 使用時のコード
<AuthImg className="figureimg" src={"CloudFrontのURL/指定のパス/xxxxx.png"} alt="xxxxx" />

 

リンク

<a>タグをカスタマイズする、はずなんですが、私は MUI を使用しているので、MUI の Link コンポーネントをカスタマイズしました。<AuthLink> というカスタムコンポーネントを作成し、<Link> の代わりに使用します。たぶん a タグも同様にすれば動くと思います。

リンク先ファイルのバイナリデータを取得し、仮の URL が作られ、それを <Link> に埋め込む動きをしています。別タブで表示する例です。

  • カスタムコンポーネント
import { fetchAuthSession } from 'aws-amplify/auth';
import { Link } from '@mui/material';

//Cognito認証付きS3へのカスタムMuiLinkコンポーネント
const AuthLink = ({ href, children, ...props }) => {
  const handleAuthClick = async (event) => {
    //通常のリンク遷移(認証なしアクセス)を防止
    event.preventDefault();
    try {
      //トークン取得
      const { tokens } = await fetchAuthSession();
      const idToken = tokens.idToken.toString();
      //認証ヘッダー付きでリクエスト
      const res = await fetch(href, {
        method: 'GET',
        headers: {
          'Authorization': `Bearer ${idToken}`
        }
      });
      if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
      //バイナリデータを取得して表示用のURLを生成
      const blob = await res.blob();
      const objectUrl = URL.createObjectURL(blob);
      //別タブで開く
      window.open(objectUrl, '_blank');
    } catch (error) {
      console.error("Authenticated link access failed:", error);
      alert("ファイルの取得に失敗しました。");
    }
  };
  return (
    <Link {...props} href={href} onClick={handleAuthClick} sx={{ cursor: 'pointer', ...props.sx }}>
      {children}
    </Link>
  );
};
  • 使用時のコード
<AuthLink href={"CloudFrontのURL/指定のパス/xxxxx.pdf"} target="_blank" rel="noopener">
  表示するテキスト
</AuthLink>

 

GET メソッド

Amazon S3 に置いた JSON データのテキストファイルを取得するのに使用しました。axios をカスタマイズして getAuthAxios という関数にしました。md ファイルのテキストデータ取得にも使えます。

  • カスタム関数
import { fetchAuthSession } from 'aws-amplify/auth';
import axios from 'axios';

//Cognito認証付きS3へのget関数
const getAuthAxios = async () => {
  const { tokens } = await fetchAuthSession();
  const idToken = tokens.idToken.toString();
  return axios.create({
    headers: {
      Authorization: `Bearer ${idToken}`
    }
  });
};
  • 使用時のコード
const [data, setData] = useState();

const getData = async () => {
  const api = await getAuthAxios();
  const res = await api.get("CloudFrontのURL/指定のパス/xxxxx.json", { cache: "no-store" });
  setData(res.data);
};

 

ストリーミング動画 (HLS)

HLS 形式のストリーミング動画を認証付きで取得するのに、react-player の config オプションを適切に設定しました。v3.4.0 だと以下のコードで動きます。ネット上の情報だと古いバージョンの情報が多かったので、気を付けてください。

  • 使用時のコード
import { fetchAuthSession } from 'aws-amplify/auth';
import ReactPlayer from 'react-player';

<div className="player-wrapper">
  <ReactPlayer
    src="CloudFrontのURL/指定のパス/xxxxx.m3u8"
    className="react-player"
    controls={true}
    width="100%"
    height="100%"
    playing={false}
    config={{
      hls: {
        xhrSetup: async function(xhr, url) {
          const { tokens } = await fetchAuthSession();
          const idToken = tokens.idToken.toString();
          xhr.setRequestHeader('Authorization', `Bearer ${idToken}`);
        }
      }
    }}
  />
</div>

react-player の詳細な設定情報は公式ドキュメントをご覧ください。

 

まとめ

いかがでしたでしょうか?

いちいちリクエストの Authorization ヘッダーにトークンを埋め込まないといけないので苦労しましたが、一度カスタムコンポーネントを作ってしまえば使い回しできるので楽ですね。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャと React を使用して社内向け e-Learning アプリ開発とコンテンツ作成に勤しんでいます。React でアプリを書き始めたら、快適すぎて他の言語には戻れなくなりました。近年は社内外への AWS 技術支援にも従事しています。AWS サービスには AWS が考える IT 設計思想が詰め込まれているので、いつも AWS を通して勉強させて頂いてまます。
取得資格:AWS 認定は15資格、IT サービスマネージャ、ITIL v3 Expert 等
2020 - 2025 Japan AWS Top Engineer 受賞
2022 - 2025 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS AppSync Events / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSアプリケーション開発クラウドソリューション
シェアする
タイトルとURLをコピーしました