39
34

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 3 years have passed since last update.

Ateam Finergy Inc.Advent Calendar 2019

Day 17

Firebase storageのセキュリティルールについて

Last updated at Posted at 2019-12-16

この記事は、株式会社エイチームフィナジーAdvent Calendar 2019 17日目です。

はじめに

今回エイチームフィナジーのAdvent Calendar 2019 17日目を担当します @okkuyama です。
最近はめっきり寒くなり外に出かけることが億劫で、もっぱら休日は自宅でお菓子作りなどをやっています。この前は米粉でドーナッツを作ったのですが、小麦とはちがいもっちりサクサクでなかなかいける味になりました。意外に簡単につくれるのでぜひ試してほしいです。


今回はReactでログイン機能があるWebアプリケーションを作る想定で、ログインしたユーザーが各々プライベートファイルをクラウドにアップする機能を実装する場合のセキュリティルールの設定方法と、注意すべき点をまとめました。

Firebase storageとは

その前のFirebase storageについて紹介。
正式にはCloud Storage for Firebaseと呼びGoogleが提供するクラウドストレージサービスで、AWSのS3に相当するサービスと言えばわかりやすいかもしれません。

価格も東京(日本)リージョンでS3と比較しても、

サービス 月額費用
(1GB/月)
参考URL
Firebase storage $0.023 https://aws.amazon.com/jp/s3/pricing/
Amazon S3 $0.025 https://cloud.google.com/storage/pricing

以上のように、ほぼ同等費用で利用可能なのでFrebaseでアプリケーションのバックエンドを組む場合は、必須のストレージサービスといえるでしょう。

Firebase storageでやりたいこと

今回クラウドストレージでのファイル管理でやりたかったのは、以下のようなセキュリティルールを敷いて、利用ユーザーがアップロードするファイルをセキュアに保ちたいと考えています。
スクリーンショット 2019-12-16 2.47.57.png

Firebase storage rulesでセキュリティルール設定

上記のセキュリティを実現するためには、以下のようなセキュリティルールを設定します。

※ セキュリティルールはWEBコンソール上からもできますがfirebase-toolsを使って、storage.rulesファイルで設定出来るようにしておくとgit管理下にできるので便利です。

service firebase.storage {
  match /b/{bucket}/o {
    match /public/{allPaths=**} {
      allow read, write: if request.auth!=null;
    }
    match /users/common/{allPaths=**} {
      allow read, write: if request.auth!=null;
    }
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth.uid == userId;
    }
  }
}

実際に構築して確認してみる

Firebaseプロジェクト作成

  • 以下のURLからGoogleアカウントでサインインしてプロジェクトの作成を行います。
  • Webアプリを作成
    • プロジェクト作成後の画面で</>アイコンをクリックしてWebアプリを作成します。
    • Webアプリ作成後に表示されるfirebaseConfigの設定項目をメモしておきます。
  • Authenticationを有効化
    • コンソールメニューのAuthenticationを選択して有効化を行ってください。
    • その後、ログイン方法でGoogleを選択して有効にしてください。
  • Storageを有効化
    • コンソールメニューのStorageを選択して有効化を行ってください。
    • Rulesの設定を聞かれるので、上記のrulesを設定してください。
  • Hostingを有効化
    • コンソールメニューのHostingを選択して有効化を行ってください。

Firebase CLIの利用

Firebaseの設定はWEBコンソールで完結できますが、設定内容をGitで管理したいのと、コマンドラインから簡単にデプロイしたいためFirebase CLIをインストールしておきます。

  • CLIインストール
    firebase-toolsをグローバルでインストールする
$ yarn global add firebase-tools
  • コンソールからFirebaseにログイン
$ firebase login

実行するとブラウザ起動後ログイン画面が表示されるのでFirebaseプロジェクトを作成したアカウントでログインを行う。

Reactアプリケーション作成

アプリ初期構築

  • Reactの構築はreact-create-appsを利用する。
$ npx create-react-app react-firebase-storage-rules-test
firebaseの初期化
  • 先程インストールしたfirebase-toolsで初期化する
$ firebase init

コマンド実行後、以下のような対話式で質問されるので次のように設定する。

  • Which Firebase CLI features do you want to setup for this folder
    • 以下の項目を上下カーソルで選択してスペースキーで選択してリターン
      • Hosting: Configure and deploy Firebase Hosting sites
      • Storage: Deploy Cloud Storage security rules
  • Select a default Firebase project for this directory
    • 先程作成したプロジェクト名を選択してリターン
  • What do you want to use as your public directory?
    • デフォルトpublicを使用、そのままリターン
  • Configure as a single-page app (rewrite all urls to /index.html)
    • Yを選択
  • File public/index.html already exists. Overwrite
    • nを選択
Firebaseモジュールのインストール
$ yarn add firebase
動作確認

取り急ぎ初期設定が完了したのでbuildしてdeployを実施してみます。

  • ビルド
$ yarn build

※ ビルドはbuildフォルダに一式保存されるため、firebase.jsonpublic項目値をbuildに変更しておきます。

firebase.json
{
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  },
  "storage": {
    "rules": "storage.rules"
  }
}
  • デプロイ
$ firebase deploy

デプロイが成功すると、コンソールにURLが表示されるのでそのURLにアクセスしてみます。

  • 以下のような画面がFirebaseのhosting先から閲覧できたらOKです。

スクリーンショット 2019-12-16 1.23.20.png

Firebase初期化

アプリを初期化できたのでいよいよFirebaseの実装に入ります。
最初にFirebaseをアプリから利用できるように初期化をしていきます。
先程FirebaseコンソールでWebアプリ作成時に取得したfirebaseConfig情報を使用してFirebaseの初期化を行います。

  • src/firebase.jsを新規作成して、以下のように設定。
src/firebase.js
import firebase from 'firebase/app'
import 'firebase/auth'     // 認証機能利用のためimport
import 'firebase/storage'  // storage利用のためimport

const firebaseConfig = {
  apiKey: "******",
  authDomain: "******.firebaseapp.com",
  projectId: "******",
  storageBucket: "******",
  appId: "******",
}

firebase.initializeApp(firebaseConfig)

export default firebase

※ 今回は簡易的な実装のため直接記載していますが、config情報は環境変数で持つようにして、設定値はGitリポジトリに載せないように注意してください。

認証機能実装

今回の実装ではログイン済みの認証ユーザーでstorageへのプライベートアクセスを行うため認証機能を先に実装していきます。
認証方法はGoogleアカウントで行うようにしています。
※事前にFirebaseコンソールでAuthenticationを有効にして、ログイン方法でGoogleを有効にしておく必要があります。

src/App.js
import React, {useState, useEffect} from 'react'
import firebase from './firebase'

const App = () => {
  const [user, setUser] = useState(null)

  // initial
  useEffect(() => {
    firebase.auth().onAuthStateChanged(user => {
      setUser(user)
    })
  }, [])

  // ログイン処理
  const handleLogin = () => {
    const provider = new firebase.auth.GoogleAuthProvider()
    firebase.auth().signInWithRedirect(provider)
  }
  // ログアウト処理
  const handleLogout = () => {
    firebase.auth().signOut()
  }

  // render
  return (
    <div>
      <p>UID: {user && user.uid}</p>
      {user ? (
        <button onClick={handleLogout}>Logout</button>
      ) : (
        <button onClick={handleLogin}>Login</button>
      )}
    </div>
  )
}

export default App

storageアップロード、ダウンロード実装

以下の要件でアップロード、ダウンロード機能を実装する。

  • HTMLのformよりファイルをFirebase storageにアップロードする。
  • storage側で保存するファイル名を指定できるようにする。
  • 指定した保存ファイル名で参照ダウンロードができる。
src/Form.js
import React, {useState} from 'react'
import firebase from './firebase'

const Form = (props) => {
  const [file, setFile] = useState(null)
  const [fileName, setFileName] = useState('sample')
  const [fileUrl, setFileUrl] = useState('')

  // storage保存ファイル名を取得
  const handleSetFileName = (e) => {
    setFileName(e.target.value)
  }
  // アップロードファイル選択
  const handleSetFile = (e) => {
    setFile(e.target.files[0])
  }
  // ファイルアップロード処理
  const handleUpload = () => {
    const storageRef = firebase.storage().ref(props.storageRef).child(`${fileName}.png`)
    storageRef.put(file)
    .then((snapshopt) => {
      console.log('file uploaded')
    })
  }
  // ファイルダウンロード処理
  const handleDownload = () => {
    const storageRef = firebase.storage().ref(props.storageRef).child(`${fileName}.png`)
    storageRef.getDownloadURL()
    .then((url) => {
      setFileUrl(url)
    })
  }

  // render
  return (
    <div>
      <div>
        <label>保存ファイル名指定</label>
        <input type="text" onChange={handleSetFileName} />
      </div>
      <div>
        <label>アップロードファイル選択</label>
        <input type="file" onChange={handleSetFile} />
        <button onClick={handleUpload}>画像アップロード</button>
      </div>
      <div>
        <label>アップロード画像表示(保存時のファイル名指定が必要)</label>
        <button onClick={handleDownload}>画像表示</button>
        <div>
          {fileUrl && <img src={fileUrl} alt="" />}
        </div>
      </div>
    </div>
  )
}

export default Form

認証パターン別にファイルアップロード・ダウンロードを検証

先程作成したFormコンポーネントを利用してApp.jsを以下のように修正します。

<Form storageRef="users/A****" />A****の箇所は実際のログインユーザーのUIDを指定が必要です。

src/App.js
import React, {useState, useEffect} from 'react'
import firebase from './firebase'
import Form from './Form'

const App = () => {
  const [user, setUser] = useState(null)

  // initial
  useEffect(() => {
    firebase.auth().onAuthStateChanged(user => {
      setUser(user)
    })
  }, [])

  // ログイン処理
  const handleLogin = () => {
    const provider = new firebase.auth.GoogleAuthProvider()
    firebase.auth().signInWithRedirect(provider)
  }
  // ログアウト処理
  const handleLogout = () => {
    firebase.auth().signOut()
  }

  // render
  return (
    <div>
      <p>UID: {user && user.uid}</p>
      {user ? (
        <button onClick={handleLogout}>Google Logout</button>
      ) : (
        <button onClick={handleLogin}>Google Login</button>
      )}
      <hr />
      <h3>公開フォルダ</h3>
      <Form storageRef="public" />
      <hr />
      <h3>認証済ユーザー共通フォルダ</h3>
      <Form storageRef="users/common" />
      <hr />
      <h3>認証済ユーザーA プライベートフォルダ</h3>
      <Form storageRef="users/A****" />
      <hr />
      <h3>認証済ユーザーB プライベートフォルダ</h3>
      <Form storageRef="users/B****" />
      <hr />
    </div>
  )
}

export default App

UI画面

yarn start実行で、以下のようなサンプル画面が表示されます。
スクリーンショット 2019-12-16 1.21.17.png

実行結果

実行した結果はrulesで設定したセキュリティルール通りの動きになりました。

フォルダ 未認証ユーザー 認証済ユーザーA 認証済ユーザーB
公開フォルダ
認証済ユーザー共通フォルダ
認証済ユーザーA
プライベートフォルダ
認証済ユーザーB
プライベートフォルダ

注意点!

token付きURLの扱いに注意する。

Firebase storageではgetDownloadURLを利用して参照可能なファイルurlの取得に認証などでセキュリティをかけれますが、取得したurl情報にはアクセストークン情報token=ab******)が付与されており、このアクセストークン付きURL情報が流出してしまうと、認証状態関係なく誰でも閲覧できてしまう恐れがあります。

以下のように末尾にtoken情報が付与される
/test.png?alt=media&token=ab******

一度流出してしまった場合は、Webコンソールからアクセストークンを取り消すを押してアクセストークンの再発行を行うと以前のトークン情報は無効となりアクセスを拒否することができます。

NGルール設定

上で設定したルールを見たときusers配下は認証が必要なので以下のようにまとめて設定できそうですが、この設定はNGとなります。

service firebase.storage {
  match /b/{bucket}/o {
    match /public/{allPaths=**} {
      allow read, write: if request.auth!=null;
    }
    match /users/{allPaths=**} {
      allow read, write: if request.auth!=null;
    }
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth.uid == userId;
    }
  }
}

一見要件通り動作しそうですが、Firebase のrulesは制約がゆるいほうが優先されるらしく、この設定だとusers配下は認証済であれば読み書きできるようになってしまいます。
つまりユーザーAの人がユーザーBのプライベートフォルダに対して読み書きが自由にできてしまうことになります。
従って多少面倒ですがrules設定は上位側でゆるくかけずに、なるべく末端に対して設定をしていく必要があります。

まとめ

Firebaseを利用することで、バックエンドをまるっと任せることができるので、アプリケーションの開発をフロントエンド側でほぼ作成していくことが可能となります。
今回紹介したstorageも手軽に扱えて、かつセキュリティルールの設定も簡単に行えるので積極的に使っていきたいサービスだと思います。
ただ、注意点でも説明したとおり、ファイル単位に付与されるアクセストークンの管理を気をつけないと、セキュリティをかけたつもりが誰でも見れるファイルになる危険性があることを認識する必要があります。
WEBアプリケーションの場合はブラウザの開発者ツールやソースコードから簡単にファイルのURL情報を取得することができるので、トークン付きURLの取り扱いは注意が必要と言えます。


株式会社エイチームフィナジーでは、「世の中からお金の不安を解消し、より人生が豊かになる社会を実現する」ことをミッションに、金融領域を中心とした様々なサービスを運営しています。

39
34
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
39
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?