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');
      //メモリリーク対策: 別タブがメモリ上の blob 読み込みを終えるのを待ってから解放する
      setTimeout(() => URL.revokeObjectURL(objectUrl), 60000);
    } 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 = () => {
  const instance = axios.create();
  instance.interceptors.request.use(async (config) => {
    const token = await getFreshIdToken(); // = (await fetchAuthSession()).tokens?.idToken?.toString()
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  });
  return instance;
};
  • 使用時のコード
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 { useLayoutEffect, useState, useEffect } from 'react';
import ReactPlayer from 'react-player';

const videoSrc = "https://xxxx.xxx.xxx/xxxx.m3u8" //ここは実際には動的に変わる想定
const [playerKey, setPlayerKey] = useState(0);

//動画ソースが変わったらReactPlayerを強制再マウント(レースコンディション対策)
useEffect(() => {
  setPlayerKey(prev => prev + 1);
}, [videoSrc]);

/* 中略 */

<ReactPlayer
  key={playerKey}
  src={videoSrc}
  className="react-player"
  controls={true}
  width="100%"
  height="100%"
  playing={false}
  config={{
    hls: {
      xhrSetup: async (xhr) => {
        //セグメント取得ごとに最新のidTokenを取得(期限切れ間近なら自動リフレッシュ)
        const { tokens } = await fetchAuthSession();
        const idToken = tokens?.idToken?.toString();
        if (idToken) {
          xhr.setRequestHeader('Authorization', `Bearer ${idToken}`);
        }
      }
    }
  }}
/>

react-player と hls.js の仕様により xhr が open 状態になる前にヘッダーをセットしようとするとエラーになってしまうことがあり、react-player の再マウント処理を入れるようにしました。この順番制御を実現できず、そのようなワークアラウンドにしました。(レースコンディション対策と書いてあるところ)

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

 

まとめ

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

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

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

著者について
広野 祐司

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

広野 祐司をフォローする

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

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

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