この記事は、株式会社エイチームフィナジー の 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でやりたいこと
今回クラウドストレージでのファイル管理でやりたかったのは、以下のようなセキュリティルールを敷いて、利用ユーザーがアップロードするファイルをセキュアに保ちたいと考えています。
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アカウントでサインインしてプロジェクトの作成を行います。
- https://firebase.google.com/?hl=ja
- プロジェクト名を入力後、規約チェックしてプロジェクト作成します。
- 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.json
のpublic
項目値をbuild
に変更しておきます。
{
"hosting": {
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
},
"storage": {
"rules": "storage.rules"
}
}
- デプロイ
$ firebase deploy
デプロイが成功すると、コンソールにURLが表示されるのでそのURLにアクセスしてみます。
- 以下のような画面がFirebaseのhosting先から閲覧できたらOKです。
Firebase初期化
アプリを初期化できたのでいよいよFirebaseの実装に入ります。
最初にFirebaseをアプリから利用できるように初期化をしていきます。
先程FirebaseコンソールでWebアプリ作成時に取得したfirebaseConfig
情報を使用してFirebaseの初期化を行います。
-
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
を有効にしておく必要があります。
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側で保存するファイル名を指定できるようにする。
- 指定した保存ファイル名で参照ダウンロードができる。
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を指定が必要です。
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
実行で、以下のようなサンプル画面が表示されます。
実行結果
実行した結果はrulesで設定したセキュリティルール通りの動きになりました。
フォルダ | 未認証ユーザー | 認証済ユーザーA | 認証済ユーザーB |
---|---|---|---|
公開フォルダ | ○ | ○ | ○ |
認証済ユーザー共通フォルダ | ✗ | ○ | ○ |
認証済ユーザーA プライベートフォルダ |
✗ | ○ | ✗ |
認証済ユーザーB プライベートフォルダ |
✗ | ✗ | ○ |
注意点!
token付きURLの扱いに注意する。
Firebase storageではgetDownloadURL
を利用して参照可能なファイルurlの取得に認証などでセキュリティをかけれますが、取得したurl情報にはアクセストークン情報(token=ab******
)が付与されており、このアクセストークン付きURL情報が流出してしまうと、認証状態関係なく誰でも閲覧できてしまう恐れがあります。
/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の取り扱いは注意が必要と言えます。
株式会社エイチームフィナジーでは、「世の中からお金の不安を解消し、より人生が豊かになる社会を実現する」ことをミッションに、金融領域を中心とした様々なサービスを運営しています。