KurocoとNuxt.jsで、ログイン画面を構築する

Kurocoを利用したNuxt.jsプロジェクトで、ログイン画面の作成方法を紹介します。
今回は例として、下記流れにてログインユーザーのみコンテンツ一覧ページが閲覧できる処理を実装します。

  • API・エンドポイントの作成
  • ログインフォーム実装
  • ログイン処理実装(APIセキュリティ毎)

前提条件

Nuxt.jsプロジェクトの作成について

このページは、KurocoとNuxt.jsでのプロジェクトが構築済みであることを前提としています。
まだNuxt.jsプロジェクトを構築していない場合、チュートリアル ->KurocoとNuxt.jsで、コンテンツ一覧ページを作成するを参照し、構築をお願いします。

APIセキュリティについて

Kurocoでは、APIのセキュリティ方法がいくつか用意されています。

Image (fetched from Gyazo)

セキュリティ「無し」を選択されている場合には、ログインの必要無くAPIからデータを取得できますが、 何らかのセキュリティを設定している場合、利用者にはフロントエンドのログインフォームから認証/認可を行っていただく必要があります。

今回は、代表的なログイン方式として、以下の2つのパターンを例にしてフロントエンドのログインフォームを構築します。

  • Cookie
  • 動的アクセストークン

セキュリティの種類については、管理画面マニュアル -> API Securityを参照してください。

セキュリティの種類の詳細な確認方法は、Swagger UIを利用して、APIのセキュリティを確認するをご確認ください。

推奨ブラウザについて

本チュートリアルは、動作確認のためGoogle Chromeの開発者ツールを利用しています。 そのため、ブラウザはGoogle Chromeを推奨いたします。

APIの設定

ログイン用のAPIの設定を行います。

APIの作成

まずはAPIを新規で作成します。
Kuroco管理画面のAPIより「新しいAPIを作成する」をクリックします。

Image (fetched from Gyazo)

API作成画面が表示されます。

Image (fetched from Gyazo)

下記入力し「追加する」をクリックします。

Image (fetched from Gyazo)

項目設定内容
タイトルlogin
1.0
ディスクリプションlogin用のAPI

APIが作成されました。

Image (fetched from Gyazo)

エンドポイントの作成

次にエンドポイントを作成します。今回は下記エンドポイントを作成します。

  • loginエンドポイント
  • profileエンドポイント
  • logoutエンドポイント
  • tokenエンドポイント(APIセキュリティが動的アクセストークンの場合のみ)

「Configure Endpoint」をクリックし、それぞれ作成します。

Image (fetched from Gyazo)

loginエンドポイントの作成

loginエンドポイントを下記設定にて作成します。

Image (fetched from Gyazo)

項目設定内容
パスlogin
カテゴリー認証
モデルlogin v1
オペレーションlogin_challenge

設定完了後、「追加」をクリックしloginエンドポイント完成です。

profileエンドポイントの作成

profileエンドポイントを下記設定にて作成します。

Image (fetched from Gyazo)

項目設定内容
パスprofile
カテゴリー認証
モデルlogin v1
オペレーションprofile
認証GroupAuth:所属しているグループ
ログインを許可するグループを選択してください。
Parameters:basic_infoemail
Parameters:basic_infoname1
Parameters:basic_infoname2

設定完了後、「追加」をクリックしエンドポイント完成です。

profileエンドポイントは、アクセスしているユーザーの情報を(簡易的に)返却するものです。
GroupAuthでの認証を設定しているため、ログイン済みで無い場合は情報を返さずにエラーとなります。

今回の場合は、email,name1,name2を値を返すように設定しており、簡易的なユーザー情報を取得するほかに、ログイン状態のリストアをする際、操作しているユーザーが本当にログイン済みであるのかを検証するためにリクエストします。

logoutエンドポイントの作成

logoutエンドポイントを下記設定にて作成します。

Image (fetched from Gyazo)

項目設定内容
パスlogout
カテゴリー認証
モデルlogin v1
オペレーションlogout
認証None

設定完了後、「追加」をクリックしエンドポイント完成です。

tokenエンドポイントの作成

tokenエンドポイントを下記設定にて作成します。

tokenエンドポイントは、APIセキュリティが動的アクセストークンの場合のみ必要になります。 APIセキュリティがCookieの場合は作成する必要はありません。

Image (fetched from Gyazo)

項目設定内容
パスtoken
カテゴリー認証
モデルlogin v1
オペレーションtoken
認証None

設定完了後、「追加」をクリックしエンドポイント完成です。

CORSの設定

次にCORSの設定をします。[CORSを設定する] をクリックします。

Image (fetched from Gyazo)

CORS_ALLOW_ORIGINSの [Add Origin] をクリックし、下記を追加します。

  • http://localhost:3000/

CORS_ALLOW_METHODSの [Add Method] をクリックし、下記を追加します。

  • GET
  • POST
  • OPTIONS

設定できたら[保存する]をクリックし、CORSの設定が完了です。

Image (fetched from Gyazo)

以上で、APIの設定が完了です。

ログインフォーム実装

次に、フロントエンドにログインフォームを作成します。

ダミーのログインフォーム実装

まずはAPIとの連携は省いた状態でログイン画面用コンポーネントの作成し、ダミーでのログイン連携処理を実装していきます。
また、お知らせ一覧画面ではログイン済みかどうかのフラグを参照し、ログイン済みでなければログイン画面に画面遷移するように変更します。

まず、ログイン画面用コンポーネントを作成します。 pages/login/index.vue ファイルを新規作成し、以下を記載してください。

<template>
    <form @submit.prevent="login">
        <input v-model="email" name="email" type="email" placeholder="email">
        <input
            v-model="password"
            name="password"
            type="password"
            placeholder="password"
        >
        <button type="submit">
            ログイン
        </button>
    </form>
</template>

<script>
export default {
    data () {
        return {
            email: '',
            password: ''
        }
    },
    methods: {
        login () {
            console.log(this.email, this.password)
        }
    }
}
</script>

この状態でnpm run devを実行し、http://localhost:3000/loginにアクセスすると簡単なログインフォームが表示されます。

Image (fetched from Gyazo)

ここまでで、一度ログの確認をします。
Chromeの開発者ツール:コンソールを開いた状態でフォームに下記を入力し、[ログイン]をクリックします。

  • email:test@example.com
  • password:password

Image (fetched from Gyazo)

すると、入力したemailとpasswordがログとしてコンソールに表示されます。

Image (fetched from Gyazo)

このログに出力された値をログイン用APIに実際にリクエストすることになります。ひとまずAPI連携部分は仮で実装をし、ログイン後の動きを確認します。

1秒間のリクエストをする見せかけのダミー処理を追加作成し、ログインリクエストに成功した場合画面上に"ログイン成功"と表示されるように、下記のように修正します。

diff --git pages/login/index.vue pages/login/index.vue
index b3a8c36..eb123b4 100644
--- pages/login/index.vue
+++ pages/login/index.vue
@@ -1,30 +1,65 @@
 <template>
     <form @submit.prevent="login">
+        <p v-if="loginStatus !== null" :style="{ color: resultMessageColor }">
+            {{ resultMessage }}
+        </p>
+
         <input v-model="email" name="email" type="email" placeholder="email">
         <input
             v-model="password"
             name="password"
             type="password"
             placeholder="password"
         >
         <button type="submit">
             ログイン
         </button>
     </form>
 </template>
 
 <script>
 export default {
     data () {
         return {
             email: '',
-            password: ''
+            password: '',
+
+            loginStatus: null,
+            resultMessage: null
+        }
+    },
+    computed: {
+        resultMessageColor () {
+            switch (this.loginStatus) {
+            case 'success':
+                return 'green'
+            case 'failure':
+                return 'red'
+            default:
+                return ''
+            }
         }
     },
     methods: {
-        login () {
-            console.log(this.email, this.password)
+        async login () {
+            // ダミーリクエスト(1秒待機の後成功/失敗する)
+            const shouldSuccess = true
+            const request = new Promise((resolve, reject) =>
+                setTimeout(
+                    () => (shouldSuccess ? resolve() : reject(Error('login failure'))),
+                    1000
+                )
+            )
+
+            try {
+                await request
+                this.loginStatus = 'success'
+                this.resultMessage = 'ログインに成功しました。'
+            } catch (e) {
+                this.loginStatus = 'failure'
+                this.resultMessage = 'ログインに失敗しました。'
+            }
         }
     }
 }
 </script>

1秒の待機の後、[ログインに成功しました。]が表示されます。 fetched from Gyazo

失敗した際にどうなるかを確認します。

ソースコードから、shouldSuccess = trueshouldSuccess = falseへ変更し、レスポンスがエラーとなる場合を再現確認します。
fetched from Gyazo

確認後は、shouldSuccess = trueへ戻してください。

ログイン状態の保持

次にログイン状態を保持できるように実装します。

a. storeの作成

まずはログイン状態をWebアプリ全体で保持しておき、他の画面でも参照できるようstoreを作成します。

store/index.jsファイルを新規作成し、下記のコードを記載してください。

export const state = () => ({
    profile: null
})

export const getters = {
    authenticated (state) {
        return state.profile !== null
    }
}

export const mutations = {
    setProfile (state, { profile }) {
        state.profile = profile
    }
}

gettersauthenticatedは、後ほど作成していくprofileデータが空かどうかでtrue/falseが返却されるものです。
profileデータが空で無ければログイン状態と判定する想定をしています。

後にログインした時やログイン状態のリストア時にprofileデータを自動取得し、それ以外のログアウトなどで値が設定されないようにしていきます。

b. middlewareの作成

次にmiddlewareを作成します。

middleware/auth.jsを新規作成し、下記のコードを記載してください。

export default async ({ app, store, redirect }) => {
    if (!store.getters.authenticated) {
        return redirect('/login')
    }
    await null
}

middlewareは各画面のソースpage/*.vueが処理をする以前に動作します。 storeのauthenticatedがfalseである場合にはログインページへ強制的にリダイレクトさせます。

c. middlewareの動作確認

middlewareの動作を確認します。 pages/login/index.vueにニュース一覧ページへのリンクを追加します。

diff --git a/pages/login/index.vue b/pages/login/index.vue
index eb123b4..37d845a 100644
--- a/pages/login/index.vue
+++ b/pages/login/index.vue
@@ -14,6 +14,12 @@
         <button type="submit">
             ログイン
         </button>
+
+        <div>
+            <nuxt-link to="/news">
+                ニュース一覧ページへ
+            </nuxt-link>
+        </div>
     </form>
 </template>

ニュース一覧画面のpages/news/index.vueのソースコードを変更して、middlewareを適用します。

diff --git a/pages/news/index.vue b/pages/news/index.vue
index ac8e0fd..dcdd806 100644
--- a/pages/news/index.vue
+++ b/pages/news/index.vue
@@ -10,6 +10,7 @@
 
 <script>
 export default {
+    middleware: 'auth',
     async asyncData ({ $axios }) {
         try {
             const response = await $axios.$get(process.env.BASE_URL + '/rcms-api/1/news')

/rcms-api/1/newsの部分はご自身のエンドポイントのURLに変更してください。
以下同様に、ソースコード内のエンドポイントURLはご自身のエンドポイントURLに変更をお願いします。

この処理により、ニュース一覧画面にアクセスするためにはログインが必要になります。 ログインしていない場合は、ニュース一覧ページへアクセスすると強制的にログイン画面へとリダイレクトされるようになります。

次に、ログイン成功時、storeprfofileをnull以外の状態に変更するようにします。 pages/login/index.vueを下記のように変更します。

diff --git a/pages/login/index.vue b/pages/login/index.vue
index 37d845a..b3cd6a1 100644
--- a/pages/login/index.vue
+++ b/pages/login/index.vue
@@ -59,6 +59,8 @@ export default {
 
             try {
                 await request
+                this.$store.commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
+
                 this.loginStatus = 'success'
                 this.resultMessage = 'ログインに成功しました。'
             } catch (e) {

ログインページにアクセスし、ログイン操作をしてニュース一覧ページに画面遷移することを確認します。
fetched from Gyazo

確認にはVue.js devtoolsを使用しています。

ログイン状態のリストアの実装

これまでの実装によって通常のログイン処理は実装されました。 しかしながら、直接URLアクセスやブラウザで画面更新されたとき、これまでの実装では一度ログインしたはずであるのにも関わらずログイン画面にリダイレクトされる不具合が発生します。

上記の操作では、storeprofileはNuxtが初期化されるためnullとなり、 直前に一度ログインしていた場合であってもログイン状態と判定されないためです。

この対応には、一度ログインしたことがある場合にはブラウザのLocalStorageにフラグを設定しておき、 フラグがtrueである場合にstoreprofileにダミーのデータを適用するようにします。

/store/index.jsを下記のように修正してください。

diff --git a/store/index.js b/store/index.js
index 1c36f1d..5cca182 100644
--- a/store/index.js
+++ b/store/index.js
@@ -13,3 +13,15 @@ export const mutations = {
         state.profile = profile
     }
 }
+
+export const actions = {
+    async restoreLoginState ({ commit }) {
+        const authenticated = JSON.parse(localStorage.getItem('authenticated'))
+
+        if (!authenticated) {
+            throw new Error('need to login')
+        }
+        commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.
+        await null
+    }
+}

また、/middleware/auth.jsを下記のように修正してください。

diff --git a/middleware/auth.js b/middleware/auth.js
index d3c7ffe..4aa086c 100644
--- a/middleware/auth.js
+++ b/middleware/auth.js
@@ -1,7 +1,9 @@
 export default async ({ app, store, redirect }) => {
     if (!store.getters.authenticated) {
-        return redirect('/login')
+        try {
+            await store.dispatch('restoreLoginState')
+        } catch (err) {
+            return redirect('/login')
+        }
     }
-
-    await null
 }

ニュース一覧ページにアクセスし、下記4点を確認します。

  • LocalStorageのauthenticatedがtrue以外である場合、ログインページにリダイレクトされること
  • LocalStorageのauthenticatedがtrueである場合、ログインページにリダイレクトされないこと
  • LocalStorageのauthenticatedがtrueかつブラウザの画面更新をした場合でも、ログインページにリダイレクトされないこと
  • LocalStorageのauthenticatedをfalseにしてブラウザの画面更新をすると、ログインページにリダイレクトされること

今回はLocalStorageの状態を、chromeの開発者ツールの[Application]タブにて確認します。
chromeの開発者ツールより[Application]タブをクリックし、[Strage] -> [Local Strage] -> [http://localhost:3000]をクリックします。

Image (fetched from Gyazo)

ログインページよりログイン後、Keyにauthenticated、Valueにtrueまたはfalseを入力し、上記4点の動作を確認します。

fetched from Gyazo

ログイン動作修正

次にログイン動作を修正します。
ログイン成功時にLocalStorageのauthenticatedをtrueにさせます。また、今後の修正に備えてログイン処理を一部storeに移動します。

/pages/login/index.vueを下記のように修正します。

diff --git a/pages/login/index.vue b/pages/login/index.vue
index b3cd6a1..25f6a8c 100644
--- a/pages/login/index.vue
+++ b/pages/login/index.vue
@@ -48,18 +48,12 @@ export default {
     },
     methods: {
         async login () {
-            // ダミーリクエスト(1秒待機の後成功/失敗する)
-            const shouldSuccess = true
-            const request = new Promise((resolve, reject) =>
-                setTimeout(
-                    () => (shouldSuccess ? resolve() : reject(Error('login failure'))),
-                    1000
-                )
-            )
-
             try {
-                await request
-                this.$store.commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
+                const payload = {
+                    email: this.email,
+                    password: this.password
+                }
+                await this.$store.dispatch('login', payload)
 
                 this.loginStatus = 'success'
                 this.resultMessage = 'ログインに成功しました。'

次に/store/index.jsを下記のように修正します。

diff --git a/store/index.js b/store/index.js
index 5cca182..b09c428 100644
--- a/store/index.js
+++ b/store/index.js
@@ -11,10 +11,29 @@ export const getters = {
 export const mutations = {
     setProfile (state, { profile }) {
         state.profile = profile
+    },
+    updateLocalStorage (state, payload) {
+        Object.entries(payload).forEach(([key, val]) => {
+            localStorage.setItem(key, val)
+        })
     }
 }
 
 export const actions = {
+    async login ({ commit }, payload) {
+        // ダミーリクエスト(1秒待機の後成功/失敗する)
+        const shouldSuccess = true
+        const request = new Promise((resolve, reject) =>
+            setTimeout(
+                () => (shouldSuccess ? resolve() : reject(Error('login failure'))),
+                1000
+            )
+        )
+        await request
+
+        commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
+        commit('updateLocalStorage', { authenticated: true })
+    },
     async restoreLoginState ({ commit }) {
         const authenticated = JSON.parse(localStorage.getItem('authenticated'))

ログイン成功時にauthenticatedがtrueになることを確認します。

fetched from Gyazo

以上でフロントエンドの実装を終了します。

次にAPIの実装を行います。 なお、実装はAPIセキュリティ毎に実装方法が変わります。 今回はAPIセキュリティがCookieの場合と、動的アクセストークンの場合の実装方法を記載します。 ご自身のAPIセキュリティに併せて、それぞれの対応方法をご確認ください。

A. ログイン処理実装(APIセキュリティがCookieの場合)

次に、先ほどダミーで作成していたログイン処理をloginエンドポイントへとアクセスするように変更します。 まずはAPIセキュリティがCookieの場合の実装方法を説明します。 Kuroco管理画面より、[API] -> [login] をクリックし、「セキュリティ」をクリックしてください。

Image (fetched from Gyazo)

「セキュリティ」よりCookieを選択し、「保存する」をクリックしてください。

Image (fetched from Gyazo)

loginエンドポイントへのリクエスト実装

store/index.jsを下記に修正します。

diff --git a/store/index.js b/store/index.js
index b09c428..45982c8 100644
--- a/store/index.js
+++ b/store/index.js
@@ -21,15 +21,7 @@ export const mutations = {
 
 export const actions = {
     async login ({ commit }, payload) {
-        // ダミーリクエスト(1秒待機の後成功/失敗する)
-        const shouldSuccess = true
-        const request = new Promise((resolve, reject) =>
-            setTimeout(
-                () => (shouldSuccess ? resolve() : reject(Error('login failure'))),
-                1000
-            )
-        )
-        await request
+        await this.$axios.$post(process.env.BASE_URL + '/rcms-api/1/login', payload)
 
         commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
         commit('updateLocalStorage', { authenticated: true })

次に、loginエンドポイントへリクエストされているか確認します。

ログインページを開き、Chromeの開発者ツール:ネットワークを開いた状態でログイン処理を行います。 すると、loginエンドポイントへとリクエストされていることが確認できます。

Image (fetched from Gyazo)

cookieの有効化

クロスオリジンでのcookieを有効化するため、nuxt.config.jsを下記のように修正してください。

diff --git a/nuxt.config.js b/nuxt.config.js
index 56cd22f..0445d45 100644
--- a/nuxt.config.js
+++ b/nuxt.config.js
@@ -51,9 +51,9 @@ export default {
 
     // Axios module configuration: https://go.nuxtjs.dev/config-axios
-     axios: {},
+     axios: {
+         baseURL: process.env.BASE_URL,
+         credentials: true,
+         withCredentials: true
+     },
 
     // Build Configuration: https://go.nuxtjs.dev/config-build

profileエンドポイントへのリクエスト/ハンドリング実装

今までの実装では、ブラウザのLocalStorageのauthenticatedフラグによってログイン済かどうかを判断する実装をしています。
しかしながら、LocalStorageはブラウザ上で簡単に改ざんが可能です。

またセッション有効期限によってauthenticatedがtrueであっても、実際には他のエンドポイントへのリクエストがアクセスエラーとなる場合もあります。
これらによる誤動作を防ぐため、profileのAPIにリクエストし、ユーザー情報が返ってくるか否かを確認することで二重のチェックを行います。

二重のチェックは、profileエンドポイントである必要はありませんが、ログイン中のユーザー名を表示する等、profileが返すデータが最初に必要になるユースケースが多いため、profileエンドポイントの利用が、スタンダードになっています。

/store/index.jsを下記のように修正します。

--- a/store/index.js
+++ b/store/index.js
@@ -24,7 +24,13 @@ export const mutations = {
 export const actions = {
   async login({ commit }, payload) {
     await this.$axios.$post(process.env.BASE_URL + '/rcms-api/1/login', payload)
-    commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.
+    const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
+    commit('setProfile', { profile: profileRes.data })
     commit('updateLocalStorage', { authenticated: true })
   },
   async restoreLoginState({ commit }) {
@@ -33,7 +39,13 @@ export const actions = {
     if (!authenticated) {
       throw new Error('need to login')
     }
-    commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.
-    await null
+
+    const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
+    commit('setProfile', { profile: profileRes.data })
   },
 }

修正ができたらリストアの動作を確認します。

ログインページを開き、Chromeの開発者ツール:アプリケーションを開いた状態でログイン処理を行います。 すると、authenticatedtrueとなります。

Image (fetched from Gyazo)

この状態で、「ニュース一覧ページへ」をクリックし画面遷移します。
今までの実装と同じように、authenticatedtrueのまま、ニュース一覧ページの表示を確認できます。

fetched from Gyazo

logoutエンドポイントへのリクエスト/ハンドリング実装

次に、ログアウト処理を実装します。

Kuroco側でセッションが残っていながらフロント側で再ログインした場合など、予期せぬ動作が発生する可能性があります。
そのため、ログイン状態ではないと判定する場合はAPIにログアウト状態にするようリクエストする必要があります。

/store/index.jsを下記のように修正します。

diff --git a/store/index.js b/store/index.js
index 296c4dc..068e184 100644
--- a/store/index.js
+++ b/store/index.js
@@ -27,13 +27,29 @@ export const actions = {
         commit('setProfile', { profile: profileRes.data })
         commit('updateLocalStorage', { authenticated: true })
     },
-    async restoreLoginState ({ commit }) {
+    async logout ({ commit }) {
+        try {
+            await this.$axios.$post(process.env.BASE_URL + '/rcms-api/1/logout')
+        } catch {
+            /** No Process */
+            /** エラーが返却されてきた場合は、結果的にログアウトできているものとみなし、これを無視します。 */
+        }
+        commit('setProfile', { profile: null })
+        commit('updateLocalStorage', { authenticated: false })
+
+        this.$router.push('/login')
+    },
+    async restoreLoginState ({ commit, dispatch }) {
         const authenticated = JSON.parse(localStorage.getItem('authenticated'))
 
         if (!authenticated) {
+            await dispatch('logout')
+            throw new Error('need to login')
+        }
+        try {
+            const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
+            commit('setProfile', { profile: profileRes.data })
+        } catch {
+            await dispatch('logout')
             throw new Error('need to login')
         }
-        const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
-        commit('setProfile', { profile: profileRes.data })
     }
 }

また、ニュース一覧画面を以下のように修正し、ログアウトボタンを作成します。

diff --git pages/news/index.vue pages/news/index.vue
index dcdd806..e79e075 100644
--- pages/news/index.vue
+++ pages/news/index.vue
@@ -1,23 +1,31 @@
 <template>
     <div>
+        <button type="button" @click="logout">
+            ログアウト
+        </button>
         <div v-for="n in response.list" :key="n.slug">
             <nuxt-link :to="'/news/'+ n.slug">
                 {{ n.ymd }} {{ n.subject }}
             </nuxt-link>
         </div>
     </div>
 </template>
 
 <script>
+import { mapActions } from 'vuex'
+
 export default {
     middleware: 'auth',
     async asyncData ({ $axios }) {
         try {
             const response = await $axios.$get(process.env.BASE_URL + '/rcms-api/1/news')
             return { response }
         } catch (e) {
             console.log(e.message)
         }
+    },
+    methods: {
+        ...mapActions(['logout'])
     }
 }
 </script>

ログイン状態のニュース一覧画面にてログアウトボタンをクリックすると、下記となることを確認します。

  • logoutエンドポイントへリクエストしている
  • ログイン画面に遷移する
  • そのままログインせずにニュース一覧画面にアクセスすると、ログイン画面に自動遷移される

fetched from Gyazo

以上でAPIセキュリティがcookieの場合のログイン処理の実装が完了です。

B. ログイン処理実装(APIセキュリティが動的アクセストークンの場合)

次に、先ほどダミーで作成していたログイン処理をloginエンドポイントへとアクセスするように変更します。 ここではAPIセキュリティが動的アクセストークンの場合の実装方法を説明します。 Kuroco管理画面より、[API] -> [login] をクリックし、「セキュリティ」をクリックしてください。

Image (fetched from Gyazo)

「セキュリティ」より動的アクセストークンを選択し、「保存する」をクリックしてください。

Image (fetched from Gyazo)

login,tokenエンドポイントへのリクエスト実装

store/index.jsを下記に修正します。

diff --git a/store/index.js b/store/index.js
index b09c428..64e6e2d 100644
--- a/store/index.js
+++ b/store/index.js
@@ -21,15 +21,11 @@ export const mutations = {
 
 export const actions = {
     async login ({ commit }, payload) {
-        // ダミーリクエスト(1秒待機の後成功/失敗する)
-        const shouldSuccess = true
-        const request = new Promise((resolve, reject) =>
-            setTimeout(
-                () => (shouldSuccess ? resolve() : reject(Error('login failure'))),
-                1000
-            )
+        const { grant_token } = await this.$axios.$post(process.env.BASE_URL + '/rcms-api/1/login', payload)
+        const { access_token } = await this.$axios.$post(
+            process.env.BASE_URL + '/rcms-api/1/token',
+            { grant_token }
         )
-        await request
 
         commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
         commit('updateLocalStorage', { authenticated: true })

loginエンドポイントとtokenエンドポイントへリクエストされているか確認します。

ログインページを開き、Chromeの開発者ツール:ネットワークを開いた状態でログイン処理を行います。 すると、loginエンドポイントとtokenエンドポイントへとリクエストされていることが確認できます。

fetched from Gyazo

tokenの保持

ここまでは、ログインしているかどうかをLocalStorageのauthenticatedのフラグ値で判定していました。
しかし、動的アクセストークンでは認証を要求するエンドポイントには実際のtoken値が必要になります。
そのため、authenticatedtokenへ変更し、token値を保持するようにします。

store/index.jsを下記に修正します。

diff --git store/index.js store/index.js
index 64e6e2d..9b048c5 100644
--- store/index.js
+++ store/index.js
@@ -1,42 +1,50 @@
 export const state = () => ({
     profile: null
 })
 
 export const getters = {
     authenticated (state) {
         return state.profile !== null
     }
 }
 
 export const mutations = {
     setProfile (state, { profile }) {
         state.profile = profile
     },
     updateLocalStorage (state, payload) {
         Object.entries(payload).forEach(([key, val]) => {
             localStorage.setItem(key, val)
         })
+    },
+    setAccessTokenOnRequestHeader (state, { rcmsApiAccessToken }) {
+        this.$axios.defaults.headers.common = {
+            'X-RCMS-API-ACCESS-TOKEN': rcmsApiAccessToken
+        }
     }
 }
 
 export const actions = {
     async login ({ commit }, payload) {
         const { grant_token } = await this.$axios.$post(process.env.BASE_URL + '/rcms-api/1/login', payload)
         const { access_token } = await this.$axios.$post(
             process.env.BASE_URL + '/rcms-api/1/token',
             { grant_token }
         )
 
+        commit('updateLocalStorage', { rcmsApiAccessToken: access_token.value })
+        commit('setAccessTokenOnRequestHeader', { rcmsApiAccessToken: access_token.value })
+
         commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
-        commit('updateLocalStorage', { authenticated: true })
     },
     async restoreLoginState ({ commit }) {
-        const authenticated = JSON.parse(localStorage.getItem('authenticated'))
+        const rcmsApiAccessToken = localStorage.getItem('rcmsApiAccessToken')
+        const authenticated = typeof rcmsApiAccessToken === 'string' && rcmsApiAccessToken.length > 0
 
         if (!authenticated) {
             throw new Error('need to login')
         }
         commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.
         await null
     }
 }

ログイン成功後の動きを確認します。

ログインページを開き、Chromeの開発者ツール:アプリケーションを開いた状態でログイン処理を行います。 すると、rcmsApiAccessTokenに値が保存されます。

fetched from Gyazo

profileエンドポイントへのリクエスト/ハンドリング実装

今までの実装では、ブラウザのLocalStorageのrcmsApiAccessTokenフラグによってログイン済かどうかを判断する実装をしています。
しかしながら、LocalStorageはブラウザ上で簡単に改ざんが可能です。

またセッション有効期限によってrcmsApiAccessTokenがtrueであっても、実際には他のエンドポイントへのリクエストがアクセスエラーとなる場合もあります。
これらによる誤動作を防ぐため、APIへアクセスすることによって、もう1クッションの追加確認をします。

そのため、/store/index.jsを下記のように修正します。

diff --git store/index.js store/index.js
index 9b048c5..c64b3a9 100644
--- store/index.js
+++ store/index.js
@@ -1,50 +1,57 @@
 export const state = () => ({
     profile: null
 })
 
 export const getters = {
     authenticated (state) {
         return state.profile !== null
     }
 }
 
 export const mutations = {
     setProfile (state, { profile }) {
         state.profile = profile
     },
     updateLocalStorage (state, payload) {
         Object.entries(payload).forEach(([key, val]) => {
             localStorage.setItem(key, val)
         })
     },
     setAccessTokenOnRequestHeader (state, { rcmsApiAccessToken }) {
         this.$axios.defaults.headers.common = {
             'X-RCMS-API-ACCESS-TOKEN': rcmsApiAccessToken
         }
     }
 }
 
 export const actions = {
     async login ({ commit }, payload) {
         const { grant_token } = await this.$axios.$post(process.env.BASE_URL + '/rcms-api/1/login', payload)
         const { access_token } = await this.$axios.$post(
             process.env.BASE_URL + '/rcms-api/1/token',
             { grant_token }
         )
 
         commit('updateLocalStorage', { rcmsApiAccessToken: access_token.value })
         commit('setAccessTokenOnRequestHeader', { rcmsApiAccessToken: access_token.value })
 
-        commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
+        const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
+        commit('setProfile', { profile: profileRes.data })
     },
     async restoreLoginState ({ commit }) {
         const rcmsApiAccessToken = localStorage.getItem('rcmsApiAccessToken')
         const authenticated = typeof rcmsApiAccessToken === 'string' && rcmsApiAccessToken.length > 0
 
         if (!authenticated) {
             throw new Error('need to login')
         }
-        commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.
-        await null
+
+        try {
+            commit('setAccessTokenOnRequestHeader', { rcmsApiAccessToken })
+            const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
+            commit('setProfile', { profile: profileRes.data })
+        } catch {
+            throw new Error('need to login')
+        }
     }
 }

ログイン後、ブラウザの画面更新をしてニュース一覧画面に遷移し、ログイン状態がリストアされることを確認します。

ログインページを開き、Chromeの開発者ツール:アプリケーションを開いた状態でログイン処理を行います。 すると、rcmsApiAccessTokenに値が保存されます。

また、この状態で、「ニュース一覧ページへ」をクリックし画面遷移しても、rcmsApiAccessTokenに値が保存されたままであることが確認できます。

fetched from Gyazo

さらに、LocalStorageのrcmsApiAccessTokenをChromeの開発者ツールより修正した場合、リストア時にログイン画面に強制的に画面遷移されることが確認できます。

fetched from Gyazo

logoutエンドポイントへのリクエスト/ハンドリング実装

次に、ログアウト処理を実装します。

Kuroco側でセッションが残っていながらフロント側で再ログインした場合など、予期せぬ動作が発生する可能性があります。
そのため、ログイン状態ではないと判定する場合はAPIにログアウト状態にするようリクエストする必要があります。

/store/index.jsを下記のように修正します。

diff --git a/store/index.js b/store/index.js
index c64b3a9..0e97247 100644
--- a/store/index.js
+++ b/store/index.js
@@ -38,19 +38,32 @@ export const actions = {
         const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
         commit('setProfile', { profile: profileRes.data })
     },
-    async restoreLoginState ({ commit }) {
+    async logout ({ commit }) {
+        try {
+            await this.$axios.$post(process.env.BASE_URL + '/rcms-api/1/logout')
+        } catch {
+            /** No Process */
+            /** エラーが返却されてきた場合は、結果的にログアウトできているものとみなし、これを無視します。 */
+        }
+        commit('setProfile', { profile: null })
+        commit('updateLocalStorage', { rcmsApiAccessToken: null })
+        commit('setAccessTokenOnRequestHeader', { rcmsApiAccessToken: null })
+
+        this.$router.push('/login')
+    },
+    async restoreLoginState ({ commit, dispatch }) {
         const rcmsApiAccessToken = localStorage.getItem('rcmsApiAccessToken')
         const authenticated = typeof rcmsApiAccessToken === 'string' && rcmsApiAccessToken.length > 0
 
         if (!authenticated) {
+            await dispatch('logout')
             throw new Error('need to login')
         }
-
         try {
             commit('setAccessTokenOnRequestHeader', { rcmsApiAccessToken })
             const profileRes = await this.$axios.$get(process.env.BASE_URL + '/rcms-api/1/profile')
             commit('setProfile', { profile: profileRes.data })
         } catch {
+            await dispatch('logout')
             throw new Error('need to login')
         }
     }

また、ニュース一覧画面を以下のように修正し、ログアウトボタンを作成します。

diff --git pages/news/index.vue pages/news/index.vue
index dcdd806..e79e075 100644
--- pages/news/index.vue
+++ pages/news/index.vue
@@ -1,23 +1,31 @@
 <template>
     <div>
+        <button type="button" @click="logout">
+            ログアウト
+        </button>
         <div v-for="n in response.list" :key="n.slug">
             <nuxt-link :to="'/news/'+ n.slug">
                 {{ n.ymd }} {{ n.subject }}
             </nuxt-link>
         </div>
     </div>
 </template>
 
 <script>
+import { mapActions } from 'vuex'
+
 export default {
     middleware: 'auth',
     async asyncData ({ $axios }) {
         try {
             const response = await $axios.$get(process.env.BASE_URL + '/rcms-api/1/news')
             return { response }
         } catch (e) {
             console.log(e.message)
         }
+    },
+    methods: {
+        ...mapActions(['logout'])
     }
 }
 </script>

ログイン状態のニュース一覧画面にてログアウトボタンをクリックすると、下記となることを確認します。

  • logoutエンドポイントへリクエストしている
  • ログイン画面に遷移する
  • そのままログインせずにニュース一覧画面にアクセスすると、ログイン画面に自動遷移される

fetched from Gyazo

以上でAPIセキュリティが動的アクセストークンの場合のログイン処理の実装が完了です。

参考

以上でKurocoを利用したNuxt.jsプロジェクトで、ログイン画面の作成方法の紹介を終わります。

今回は基本的な説明のため、簡単にログイン画面を作成して最低限のログイン制御を実現しました。 実際に利用する際には、フォームのバリデーション処理や、@nuxt/auth などのライブラリをご利用いただく必要が出てくるかと思いますが、基本的なログイン構築の流れの理解としてご利用いただければ幸いです。

お探しのページは見つかりましたか?解決しない場合は、問い合わせフォームからお問い合わせいただくか、Slackコミュニティにご参加ください。