12
15

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.

Firebase Hostingの1つのプロジェクトでUser向けサイトとAdmin向けサイトを運営する 〜で、Basic認証もする〜

12
Posted at

Firebase Hostingは簡単なサイトを公開するには簡単で便利なサービスです。
ここでは1つのプロジェクトにて復数のサイトを運営し、かつ、それぞれにBaic認証等をかける方法についてまとめてみます。

やりたいこと

  • 1つのプロジェクトでUser向けサイトとAdmin向けサイトを運営する
  • 各サイトのコンテンツはSPA(ReactかつReactRouterを利用する)
  • 各サイトに別々にBasic認証をかけたい(アカウント、有効無効を分けたい)

手順

  • Hostingにて復数サイトを作成し、それぞれにtargetを設定する
  • 各targetにて、全てのリクエストをfunctionsに仕向けるようにする
  • user用はappUser, admin用はappAdmin functionに仕向ける
  • 各functionで表示する(SPA)コンテンツを指定する
  • 各functionにてBasic認証の設定をする

前提

  • Firebaseのアカウント、プロジェクトが作成されHostingとFunctionsが利用できる状態となっている
    • ソースロケーションはasia-northeast1(必須ではない)
  • firebase-toolsが利用できる状態にある

実装:準備

まず、作業場所を作ります。普通にmkdir。

mkdir web-test
cd web-test

で、firebase init。
とりあえずFunctionsとHostingを選んでおきます。

firebase init

? Which Firebase CLI features do you want to set up for this folder? Press Space
 to select features, then Enter to confirm your choices.
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◉ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features

existingプロジェクトを選びます。お好みで。

? Please select an option: (Use arrow keys)
❯ Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project

ここではmulti-site-43c8bというプロジェクトを使います。

? Select a default Firebase project for this directory:
❯ multi-site-43c8b (multi-site)

とりあえずJavaScript。

? What language would you like to use to write Cloud Functions? (Use arrow keys)

❯ JavaScript
  TypeScript

Lintはいらない。dependenciesはインストールしておきます。

? Do you want to use ESLint to catch probable bugs and enforce style? No
? Do you want to install dependencies with npm now? Yes

publicのままで。

? What do you want to use as your public directory? (public)

どっちでもいいのですが、生成されるfirebase.jsonにrewrite記述を入れておきたいのでyを選択。

? Configure as a single-page app (rewrite all urls to /index.html)? (y/N) y

こんな構造になっているはず。

.
├── firebase.json
├── functions
│   ├── index.js
│   ├── node_modules
│   ├── package-lock.json
│   └── package.json
└── public
    └── index.html

実装:サイトを構成する

では復数サイト(ここでは2つ)を構成していきます。

サイトの追加

既にデフォルトサイトはあるので、1つサイトを追加します。Hostingページの下の方に書きボタンがあります。

スクリーンショット 2019-12-06 7.49.35.png

ProjectIdがサブドメインになっているので、-adminを追加したサイトを追加してみます。

スクリーンショット 2019-12-06 7.50.13.png

1つ追加され、2つのサイトが確認できます。

スクリーンショット 2019-12-06 7.50.41.png

サイトの構成(target設定)

コマンドで各サイトを管理・操作するためにtarget名を設定しておきます。

firebase target:apply hosting [ターゲット名] [サイト名]

の形式で設定できます。
デフォルトのサイトにuser-site, 追加したadmin用(を想定した)サイトにadmin-siteと名前を付けます。

firebase target:apply hosting user-site fire
firebase target:apply hosting admin-site multi-site-43c8b-admin

.firebasercが下記のようになっています。

.firebaserc
{
  "projects": {
    "default": "multi-site-43c8b"
  },
  "targets": {
    "multi-site-43c8b": {
      "hosting": {
        "user-site": [
          "multi-site-42c7a"
        ],
        "admin-site": [
          "multi-site-42c7a-admin"
        ]
      }
    }
  }
}

firebase.jsonの設定

ターゲットが設定できたところ、各サイトをただしく管理できるよう、firebase.jsonの設定を下記のように変更します。
復数サイトの情報を管理する場合は、hosting要素を配列に、各サイトの設定を行っていきます。
いろいろ書いていますが、、

  • user-siteの公開ディレクトリをpublicに、全てのリクエストをその中のindex.htmlで処理(SPA想定)
  • admin-siteの公開ディレクトリをpublic-adminに、全てのリクエストをその中のindex.htmlで処理(SPA想定)

というようなところがポイントです。

firebase.json
{
  "hosting": [
    {
      "target": "user-site",
      "public": "public",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "admin-site",
      "public": "public-admin",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }
  ]
}

動作確認

では各サイトに実際にコンテンツを配置・デプロイして動作を確認してみます。
publicおよびpublic-admin内に簡単なindex.htmlを作成してデプロイしてみます。

public/index.html

ユーザーサイト向けHTMLの内容。

index.html
<!DOCTYPE html>
<html>
  <body>User-Site</body>
</html>

public-admin/index.html

public-adminディレクトリは存在しないので、作成します。

mkdir public-admin
touch public-admin/index.html

adminサイト向けのHTMLの内容。

index.html
<!DOCTYPE html>
<html>
  <body>Admin-Site</body>
</html>

現状でこんな構造かと思います。

.
├── firebase.json
├── functions
│   ├── index.js
│   ├── node_modules
│   ├── package-lock.json
│   └── package.json
├── public
│   └── index.html
└── public-admin
    └── index.html

deploy

準備ができたらdeployしてみます。

firebase deploy

今回はHosting関連だけの更新なのでfirebase deploy --only hostingでもかまいません。

deployが完了すると下記のようにURLが表示されますので、それぞれにアクセスしてみてください。

URLは各自の環境で違います。

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/multi-site-43c8b/overview
Hosting URL: https://multi-site-43c8b.firebaseapp.com
Hosting URL: https://multi-site-43c8b-admin.firebaseapp.com

それぞれのURLでユーザー用、アドミン用のコンテンツが見えるはずです。

復数サイトを公開したいだけなら、ここまでの操作でいいのですが、実運用を前提とすると各サイトにBasic認証等をかける必要があります(他にもいろいろありますが)。ただ、現状、Firebase Hostingではそれらの機能が存在しないためFunctions等の機能を応用して実現する必要があります。

具体的にはHTTP/HTTPSリクエストをFunctionsに振り分けて、そこで認証処理を行ったのち、Webコンテンツにリダイレクト(正確には違います)するような感じです。
下記のようなイメージでしょうか。

[リクエスト] → [functions(認証:OKなら)] → [コンテンツ表示]

実装:リクエストをfunctionsに仕向ける

では、設定していきます。
まず、Functionsのプログラム内で利用するモジュールをインストールしておきます。

作業はfunctionsフォルダの中で行ってください。のちのち使うモジュールも入れておきます。

cd functions
npm install --save express path basic-auth-connect

exprssの機能をつかって実装します。
ユーザーサイトを処理するexpressインスタンスとアドミンサイトを処理するインスタンスをを作成し、レスポンスを返すようにしています。

index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const basicAuth = require('basic-auth-connect');

const appUser = express();
const appAdmin = express();

//userサイトの処理
appUser.all("*", (req, res, next) => {
    res.send("user site from function");
})

//adminサイトの処理
appAdmin.all("*", (req, res, next) => {
    res.send("admin site from function");
})

//関数公開
exports.appUser = functions.https.onRequest(appUser);
exports.appAdmin = functions.https.onRequest(appAdmin);

リスエストがFunctionsに割り振られるようにfirebase.jsonの設定を変更します。

firebase.json
{
  "hosting": [
    {
      "target": "user-site",
      "public": "public",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
+          "function": "appUser"
        }
      ]
    },
    {
      "target": "admin-site",
      "public": "public-admin",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
+          "function": "appAdmin"
        }
      ]
    }
  ]
}

なお、"public"で指定したフォルダ内にコンテンツがあると、仕向設定をしてもそちらが優先されるようなので、publicおよびpublic-adminの中のコンテンツ(index.html)は削除しておきます。

cd ..
rm public/index.html
rm public-admin/index.html

作業が終わればデプロイします。

firebase deploy

なお、functionは明示的にリージョンを指定しないとus-central1にデプロイされます。リージョンを指定することもできますが、HTTPのrewirte先となるfunctionはus-central1である必要があるので、一旦そのままで大丈夫です。詳しくはこちら

デプロイが完了したら表示を確認してみてください。

実装:表示先をSPA(React)コンテンツにする

上記サンプルではダイレクトに文字列を返していただけですが、実運用を想定してSPAコンテンツを返してみます。
functionsフォルダ内でreactプロジェクトを作成してみます。

VueやAngularでも基本的には同じです。

ユーザー用とアドミン用のreactアプリを生成します。

cd functions
create-react-app user-site --use-npm
create-react-app admin-site --use-npm

デフォルト画面のままでもテストはできますが、振り分けがちゃんとできているか確認しづらいのでそれぞれのApp.jsを編集します。

user-site/src/App.js

user-site/src/App.js
import React from 'react';
import './App.css';

function App() {
  return (
    <div>
      <p>User-Site from React</p>
    </div>
  );
}

export default App;

admin-site/src/App.js

admin-site/src/App.js
import React from 'react';
import './App.css';

function App() {
  return (
    <div>
      <p>Admin-Site from React</p>
    </div>
  );
}

export default App;

編集が終わったら、user-site, admin-site各フォルダ内でbuildを実行します。

npm run build

現状で以下のような構造かと思います。
user-site, admin-site以下にbuildフォルダが生成されていることを確認してください。

.
├── firebase.json
├── functions
│   ├── admin-site
│   │   ├── README.md
│   │   ├── build #admin公開サイト
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── public
│   │   └── src
│   ├── index.js
│   ├── package-lock.json
│   ├── package.json
│   └── user-site
│       ├── README.md
│       ├── build #user公開サイト
│       ├── package-lock.json
│       ├── package.json
│       ├── public
│       └── src
├── public
└── public-admin

各サイト内のコンテンツを正しく表示できるようにfunction/index.jsを編集します。

index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const basicAuth = require('basic-auth-connect');

const appUser = express();
const appAdmin = express();

//userサイトの処理 ----------------------------------------------------------

//staticコンテンツ指定
appUser.use(express.static(path.join(__dirname, 'user-site', 'build')));
//すべてのリクエストをuser-site/build/index.htmlで処理する
appUser.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'user-site', 'build', 'index.html'));
})

//adminサイトの処理 ----------------------------------------------------------

//staticコンテンツ指定
appAdmin.use(express.static(path.join(__dirname, 'admin-site', 'build')));

//すべてのリクエストをadmin-site/build/index.htmlで処理する
appAdmin.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'admin-site', 'build', 'index.html'));
})

//関数公開
exports.appUser = functions.https.onRequest(appUser);
exports.appAdmin = functions.https.onRequest(appAdmin);

編集が終わったらデプロイします。

firebase deploy

アクセスした際、Permissionエラーがでる場合があります。その場合、GCPコンソールのCloud Functionsで、ユーザーとパーミッションを設定します。

スクリーンショット 2019-12-06 9.35.38.png

[ユーザーの追加]でallUsersを選び、Cloud Functions 起動元の権限を与えます。

スクリーンショット 2019-12-06 9.38.32.png

設定が完了したら再度アクセスしてみてください。
Reactが生成するコンテンツが表示されるはずです。

実装:Basic認証をかける

では、Basic認証をかけてみます。
ここまでの作業がうまくいっていれば、追加は簡単です。

ここではuser-siteのみにBasic認証をかけてみます。

index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const basicAuth = require('basic-auth-connect');

const appUser = express();
const appAdmin = express();

//userサイトの処理 ----------------------------------------------------------

//basic auth
+appUser.use(basicAuth('user','password'));

//staticコンテンツ指定
appUser.use(express.static(path.join(__dirname, 'user-site', 'build')));
//すべてのリクエストをuser-site/build/index.htmlで処理する
appUser.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'user-site', 'build', 'index.html'));
})

//adminサイトの処理 ----------------------------------------------------------

//staticコンテンツ指定
appAdmin.use(express.static(path.join(__dirname, 'admin-site', 'build')));

//すべてのリクエストをadmin-site/build/index.htmlで処理する
appAdmin.use((req, res, next) => {
    res.sendFile(path.join(__dirname, 'admin-site', 'build', 'index.html'));
})

//関数公開
exports.appUser = functions.https.onRequest(appUser);
exports.appAdmin = functions.https.onRequest(appAdmin);

デプロイして動作を確認します。

firebase deploy

Functionsを経由しないようにする

functionsはしばらく利用しないと眠ってしまうのと、無駄にus-central1を経由するのでレスポンスが落ちます(そのはず)。
なので、本番環境に切り替える場合はfunctionsを経由しないよう、直接コンテンツを見に行くようにしたほうがいいでしょう。

firebase.js
{
  "hosting": [
    {
      "target": "user-site",
+      "public": "user-site/build",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
+          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "admin-site",
      "public": "public-admin",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "function": "appAdmin"
        }
      ]
    }
  ]
}

かるく計測してみたところ、Functions経由なしでは10ms以下でresponseが完了しますが、Functions経由だと200ms前後かかるようです。

React-Routerが正しくルーティングされているか確認する

SPAの場合、ルート(/)ではうまく動いても、それ以外では動かないということがよくあります。
自分が利用しているフレームワークやルーティングモジュールで正しく動作するか確認したほうが無難です。

ここではreact-router利用下で正しくルーティングが動作するか確認してみます。

user-site, admin-siteそれぞれの下で、

npm install --save react-router-dom

を実行して、react-routerが利用できるようにします。そのうえで、各、App.jsを編集します。

user-site/src/App.js

user-site/App.js
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" render={()=><p>Home@User</p>}/>
        <Route exact path="/about" render={()=><p>About@User</p>}/>
        <Route render={()=><p>Page not found@User</p>}/>
      </Switch>
    </BrowserRouter>
  );
}

export default App;

admin-site/src/App.js

admin-site/App.js
import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/" render={()=><p>Home@Admin</p>}/>
        <Route exact path="/about" render={()=><p>About@Admin</p>}/>
        <Route render={()=><p>Page not found@Admin</p>}/>
      </Switch>
    </BrowserRouter>
  );
}

export default App;

編集が終わったらそれそれのアプリ内でbuildを実行します。

npm run build

buidが完了したらデプロイします。

firebase deploy

動作を確認してみます。
各サイトで、/aboutや/xxxxとした際に正しく動作してるか確認してみてください。

とりあえず以上です。

12
15
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
12
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?