Vueでページ毎にアクセス権を設定する方法

記事
IT・テクノロジー

Vueでページ毎にアクセス権を設定する方法

Vue と Firebase を組み合わせて、利用者限定のサービスを実装する方法を紹介しました。実際の Web アプリケーションでは、アクセスできる人を限定したページと、誰でもアクセスできるページが混在する場合もたくさんあります。例えば、ブログのようなサイトを考えた場合は、記事を投稿するのは「管理者」だけで、閲覧は誰でもできるとうケースもたくさんあります。この記事では、ページ毎にアクセス権を設定する方法を紹介しています。

Vue プロジェクトの作成

最初に、Vue のサンプルプロジェクトを作成します。 今回の実装例では、

* Vue Router
* Pinia を利用します。 最初にサンプルプロジェクトを作成する際に組み込んで起きます。サンプルプロジェクトの作成にはViteを利用しています。
% npm init vue

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue-firebase-sample
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /home/guest/vue-firebase-sample...

Done. Now run:

  cd vue-firebase-sample
  npm install
  npm run dev

npm notice
npm notice New minor version of npm available! 8.5.0 -> 8.6.0
npm notice Changelog: h t t ps://github.com/npm/cli/releases/tag/v8.6.0
npm notice Run npm install -g npm@8.6.0 to update!
npm notice
%
プロジェクトを作成した後は、必要なパッケージをインストールします。

% cd vue-firebase-sample
% npm install
続いて不要なファイルを削除します。

% rm -rf src/compornets/*
% rm src/assets/logo.svg
% rm src/assets/base.css
% rm src/stores/counter.js
次に、src/App.vueとsrc/HomeView.vueを更新します。

(src/App.vue)
<script setup>
import { RouterLink, RouterView } from "vue-router";
</script>

<template>
  <header>
    <div class="wrapper">
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>
(src/views/HomeView.vue)
<script setup></script>

<template>
  <main>
    <h1>Home</h1>
  </main>
</template>
(*)CSS の部分は省略しています。

Firebase の準備

次は、Firebase を使うための準備です。 詳細は別の記事で紹介していますので、そちらを参照してください。 やることは:

* Google アカウントの準備
* Firebase コンソールで Firebase プロジェクトの作成
* 利用するログイン方法の設定
* 利用するユーザーの作成
ログイン機能が利用可能なドメインの設定 です。 その上で、Firebase のプロジェクト情報を取得して、Firebase の初期化のための Javascript のプログラムを作成します。
// (src/lib/firebase.js の例)
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
// TODO: Add SDKs for Firebase products that you want to use
// h t t p s://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  // (src/lib/firebase.js の例)
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
// TODO: Add SDKs for Firebase products that you want to use
// h t t p s://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxAbc",
  authDomain: "test-xxxxx.firebaseapp.com",
  databaseURL: " h t t p s://test-xxxxx.firebaseio.com",
  projectId: "test-xxxxx",
  storageBucket: "test-xxxxx.appspot.com",
  messagingSenderId: "1121252635433",
  appId: "1:1010101010101:web:29axxxxxxxxxxxxxxxz208",
};

// Initialize Firebase
export const firebase = initializeApp(firebaseConfig);
export const auth = getAuth();
};

// Initialize Firebase
export const firebase = initializeApp(firebaseConfig);
export const auth = getAuth();
最後に Firebase のモジュールをインストールしておきます。

% npm install firebase

実装例

まず、Firebase のログイン(ユーザー認証)の状態を一括管理できるように、Piniaを利用してstoreを作成します。

// src/stores/loign.js
import { defineStore } from "pinia";

export const useLoginStore = defineStore({
  id: "login",
  state: () => ({
    loginState: false,
    email: "",
    uid: "",
  }),
  actions: {
    login(email, uid) {
      this.loginState = true;
      this.email = email;
      this.uid = uid;
    },
    logout() {
      this.loginState = false;
      this.email = "";
      this.uid = "";
    },
  },
});

一括管理する理由は、ページ毎にログイン状態を管理するよりは、サイト全体でログインの状態を共有した方が効率が良くなるためです。以前の例では、全てのページのアクセスを「ログインしている人限定」という想定で実装したので、App.vueのページのみで管理しても大きな問題ではありませんでした。

今回は、「ページ毎に利用制限が違う」という前提なので、どのページからでもログインの状態が確認できるように一括管理する方法を採用しました。

次に、App.vueの例です。

(src/App.vueの例)
<script>
import { RouterLink, RouterView } from "vue-router";
import {
  browserSessionPersistence,
  onAuthStateChanged,
  setPersistence,
} from "firebase/auth";
import { auth } from "./lib/firebase";
import { useLoginStore } from "./stores/login";
export default {
  setup() {
    const login = useLoginStore();
    return {
      login,
    };
  },
  mounted() {
    setPersistence(auth, browserSessionPersistence)
      .then(() => {
        const user = auth.currentUser;
        if (user) {
          // sets signed in status
          this.login.login(auth, user.email, user.uid);
        } else {
          // sets signed out status
          this.login.logout();
        }
      })
      .catch((error) => {
        // Handle Errors here.
        const errorCode = error.code;
        const errorMessage = error.message;
        // force to sign out
        this.login.logout();
      });
    onAuthStateChanged(auth, (user) => {
      if (user) {
        // Sign in
        this.login.login(auth, user.email, user.id);
      } else {
        // Sign out
        this.login.logout();
      }
    });
  },
};
</script>
<template>
  <header>
    <div class="wrapper">
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>
  <RouterView />
</template>
<style>
最初に、この部品が Web ブラウザで表示できる様になった段階(マウントされた段階)で、以前のログイン状態を Firebase の機能(setPersistence())を利用して判別して、一括管理しているログインの状態を更新します。 この機能は、Web ブラウザに保存された「クッキー(cookie)」を利用して判別しています。この設定の場合、Web ブラウザの同じ「タブ」を使ってアクセスしている場合は、以前のログインの状態を覚えておいてログインの処理を行わなくて良いようになっています。

また、ログアウトなどによって、Firebase のログイン状態が変化した場合も、「onAuthStateChanged()」を利用して、一括管理しているログインの状態を更新しています。

実際の表示は、Vue Router の機能を利用して、必要な「ページ」を表示するようにしています。この例では、viewsフォルダの下の「HomeView.vue」と「AboutView.vue」の二つのページを表示できるようにしています。

この例では、「HomeView.vue」は誰でもアクセスできる様にしているので特別な記述は必要ありません。

「AboutViewvue」は、ログインした利用者のみ閲覧可能にするために、記述を変更します。

ログインしていない場合には、「ログインのフォーム」を表示して、それ以外の場合は、「This is an about page」を表示する様にしています。

AboutView.vueの例です。

(src/views/AboutView.vueの例)
<template>
  <div v-if="login.loginState" class="about">
    <h1>This is an about page</h1>
  </div>
  <div v-if="!login.loginState">
    <LoginForm />
  </div>
</template>
<script>
import { useLoginStore } from "../stores/login";
import LoginForm from "../components/loginForm.vue";
export default {
  components: { LoginForm },
  setup() {
    const login = useLoginStore();
    return {
      login,
    };
  },
};
</script>
公式チュートリアルで学習した「v-if」で一括管理しているログインの状態によって、「ログインフォーム(LoginForm)」か「This is an about page」の表示を切り替えています。

ログインフォームの例

ログインフォームの例です。以前に紹介した、全てのページでログインが必要な場合は、ログインフォーム(子)と呼び出し側のページ(親)の間でデータのやり取りをしてログインの処理を行う方法を採用していました。

今回は、ページ毎に必要に応じてログインホームを利用する形になるため、親と子でデータを受け渡しする方法を採用すると必要なページ全てでログインの処理をする必要が出てきます。当然、同じようなコードを必要なページに書く必要があります。関数を利用すれば大きな問題ではありませんが、今回は別の実装方法を紹介しておきます。

今回の方法は、ログインフォームから一括管理しているログインの情報を更新するようにして、データのやり取りをしないで済むようにするやり方です。この方法だと、ログインフォームを呼び出すだけで済むのでログイン処理のためのコードを毎回書く必要がありません。

まずは、ログインフォームの例です。

(src/components/loginForm.vueの例)
<template>
  <div>
    <form>
      <div class="form-group">
        <label>E-Mail</label>
        <input class="form-control" type="email" v-model="loginInfo.email" />
      </div>
      <div class="form-group">
        <label>Password</label>
        <input
          class="form-control"
          type="password"
          v-model="loginInfo.password"
        />
      </div>
      <button class="btn btn-primary" type="button" @click="loginEvent()">
        Sign In
      </button>
    </form>
  </div>
</template>
<script>
import { useLoginStore } from "../stores/login";
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "../lib/firebase";
export default {
  setup() {
    const login = useLoginStore();
    return {
      login,
    };
  },
  data() {
    return {
      loginInfo: {
        email: "",
        password: "",
      },
    };
  },
  methods: {
    loginEvent() {
      console.log("loginEvent():", this.loginInfo);
      signInWithEmailAndPassword(
        auth,
        this.loginInfo.email,
        this.loginInfo.password
      )
        .then((credential) => {
          this.login.login(credential.user.email, credential.user.uid);
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>
「Sign In」のボタンが押されると、フォームに入力された情報を取り込んで、Firebase のログイン処理を行うようになっています。ログインが成功すると一括管理しているログインの状態を更新する様になっています。 Firebase 上でのログイン状態が変わると、すでにApp.vueで組み込んでいる。onAuthStateChanged()が自動的に一括管理しているログインの状態を更新する様になっているので、アクセスが制限されているページにアクセス中でも、ログアウトされた時点で表示はログインフォームに切り替わるようになっています。

このように実装することで、ログインフォームは一括管理しているログイン状態と連携して機能するようにできます。

このアプリだけを考えた場合、全体の設計をシンプルにすることが可能です。

ただ、このログインフォームを別のアプリケーションで利用する事を考えた場合、別のアプリケーションでも全く同じ「src/stores/login.js」を実装する必要があります。データの受け渡しをして、処理機能を実装しない方がいろいろな場所で利用しやすくなります。どちらの実装が良いかは、「部品」をどのように共用するかの方針によって変わってきます。これ以外にも、同じ様な機能を実現する実装のやり方もありますので、いろいろ試して自分なりの部品の共用の方針を作るのが良いかと思います。

まとめ

この記事では、ページによって利用者の制限を変えたい場合の実装方法を紹介しました。 同じ、利用者限定の実装でも、実際に必要な状況によって実装のやり方が変わってきます。以前の例では、シンプルに一つの Vue の部品でログインの状態を管理する方法を採用していました。この例では、一括管理してログインの状態をページ間で共有できる方法で実装しています。似たような機能の実装ですが、アプリケーションによって実装のやり方や部品化の方針も変わってきます。特に、部品化の場合には、今回の例のように使い方の制限が変わってくる場合が多くなります。この例では、部品とPiniaのストアの実装をペアにして利用する必要があります。共有可能な部品が増えてくると、こうした依存関係を全て覚えておくのは難しくなります。効率よく既に作成した部品を再利用できるように部品のドキュメントをきちんと作成しておくと将来の開発で役に立ちます。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す