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

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

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

前提条件

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

このページはKurocoとNuxt.jsでのプロジェクトが構築済みであり、コンテンツ一覧のページが作成されていることを前提としています。 まだ構築していない場合は、下記のチュートリアルを参照してください。
Kurocoビギナーズガイド
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より「追加」をクリックします。

Image from Gyazo

API作成画面が表示されるので、下記入力し「追加する」をクリックします。

Image from Gyazo

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

APIが作成されました。

Image from Gyazo

エンドポイントの作成

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

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

「新しいエンドポイントの追加」をクリックし、それぞれ作成します。

Image from Gyazo

loginエンドポイントの作成

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

Image from Gyazo

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

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

profileエンドポイントの作成

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

Image from Gyazo Image from Gyazo

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

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

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

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

logoutエンドポイントの作成

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

Image from Gyazo

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

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

tokenエンドポイントの作成

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

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

Image from Gyazo

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

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

CORSの設定

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

Image from Gyazo

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

  • http://localhost:3000/
  • フロントエンドドメイン

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

  • GET
  • POST
  • OPTIONS

CORS_ALLOW_REDENTIALSの[Allow Credentials]にチェックが入っていることを確認します。

Image from Gyazo

問題なければ [保存する] をクリックします。
以上で、APIの設定が完了です。

ログインフォーム実装

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

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

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

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

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 from Gyazo

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

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

Image from Gyazo

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

Image from Gyazo

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

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

diff --git a/pages/login/index.vue b/pages/login/index.vue
index 44146fc..492b108 100644
--- a/pages/login/index.vue
+++ b/pages/login/index.vue
@@ -1,28 +1,63 @@
 <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秒の待機の後、[ログインに成功しました]が表示されます。

Image from Gyazo

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

ソースコードから、shouldSuccess = trueshouldSuccess = falseへ変更し、レスポンスがエラーとなる場合を再現確認します。
Image 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 }) {
         return {
             response: await $axios.$get('/rcms-api/4/news'),

/rcms-api/4/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('/rcms-api/9/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('/rcms-api/9/login', payload)

-    commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.state.profileに適用
+    const profileRes = await this.$axios.$get('/rcms-api/9/profile')
+    commit('setProfile', { profile: profileRes.data })
     commit('updateLocalStorage', { authenticated: true })
   },
   async restoreLoginState({ commit }) {
     const authenticated = JSON.parse(localStorage.getItem('authenticated'))

     if (!authenticated) {
       throw new Error('need to login')
     }
-    commit('setProfile', { profile: {} }) // ダミーのオブジェクトをstore.
-    await null
+
+    const profileRes = await this.$axios.$get('/rcms-api/9/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('/rcms-api/9/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('/rcms-api/9/profile')
+            commit('setProfile', { profile: profileRes.data })
+        } catch {
+            await dispatch('logout')
             throw new Error('need to login')
         }
-        const profileRes = await this.$axios.$get('/rcms-api/9/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>
         <p>ニュース一覧ページ</p>
+        <button type="button" @click="logout">
+            ログアウト
+        </button>
         <div v-for="n in response.list" :key="n.slug">
             <nuxt-link :to="`/news/${n.topics_id}`">
                 {{ n.ymd }} {{ n.subject }}
             </nuxt-link>
         </div>
     </div>
 </template>
 
 <script>
+import { mapActions } from 'vuex';
+
 export default {
     middleware: 'auth',
     async asyncData ({ $axios }) {
         return {
             response: await $axios.$get('/rcms-api/4/news'),
         };
     },
+    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('/rcms-api/9/login', payload)
+        const { access_token } = await this.$axios.$post(
+            '/rcms-api/9/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('/rcms-api/9/login', payload)
         const { access_token } = await this.$axios.$post(
             '/rcms-api/9/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('/rcms-api/9/login', payload)
         const { access_token } = await this.$axios.$post(
             '/rcms-api/9/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('/rcms-api/9/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('/rcms-api/9/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('/rcms-api/9/profile')
         commit('setProfile', { profile: profileRes.data })
     },
-    async restoreLoginState ({ commit }) {
+    async logout ({ commit }) {
+        try {
+            await this.$axios.$post('/rcms-api/9/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('/rcms-api/9/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>
         <p>ニュース一覧ページ</p>
+        <button type="button" @click="logout">
+            ログアウト
+        </button>
         <div v-for="n in response.list" :key="n.slug">
             <nuxt-link :to="`/news/${n.topics_id}`">
                 {{ n.ymd }} {{ n.subject }}
             </nuxt-link>
         </div>
     </div>
 </template>
 
 <script>
+import { mapActions } from 'vuex';
+
 export default {
     middleware: 'auth',
     async asyncData ({ $axios }) {
         return {
             response: await $axios.$get('/rcms-api/4/news'),
         };
     },
+    methods: {
+        ...mapActions(['logout'])
+    },
 };
 </script>

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

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

fetched from Gyazo

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

参考

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

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

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