Help us understand the problem. What is going on with this article?

Python Django チュートリアル SPA編(6)

More than 1 year has passed since last update.

勉強会用の資料です.
今回の記事ではユーザログイン情報の確認や復元を行えるようにしていきます.

ソースコードはgithubで管理してます.
https://github.com/usa-mimi/tutorial-spa
今回の記事から開始する人は tutorial6-start のタグから開始してください.

はじめに

前回の記事までで,ログイン画面で正しいIDとパスワードを入力するとログイン状態になるところまで作成しました.
ユーザがログインしているかどうかの情報は axiosクライアント(this.$request)にtokenがセットされているかどうかで一応の判断はできますが,
ユーザがログイン済みかどうか という情報の判断をAPI用に使用しているプラグインに問い合わせるのは好ましくありません.
また,今はまだないですが,ログイン中のユーザの名前などは多くのコンポーネントで使用することが想定されるため,
どのコンポーネントのdataで定義すべきなのか判断がつきません.

vue.jsには vuex と呼ばれるライブラリが提供されており,
これを使うことでコンポーネント間で共通して使用するデータを楽に共有することが可能になります.

vuexのパターン図

上述の公式の図を見てわかる通り,vuexはvueのデータ処理部分を抜き出して共通化したライブラリと言えます.
vueコンポーネントでは通常,data でデータを定義し, テンプレート内で記述したボタン等のhtmlに
methods で定義したメソッドを紐付けます.
呼び出されたメソッド内ではデータを変更し,テンプレート内で使用されているデータに対して変更を反映します.

vuexではデータを state,メソッドを action として定義します.
また,vuex内で mutation と呼ばれるメソッドを持っており, stateを変更できるのはmutationのみ,と設定されています.
少し回りくどいように感じるかもしれませんが,このような設計によってAPIのような非同期な処理を同期的な処理に落とし込めるようになっています.

公式ドキュメントに詳しい思想やカウンタでのサンプルコードが記載されているのでぜひ一度目を通してください.
https://vuex.vuejs.org/ja/getting-started.html

ログイン情報の確認

vuexのインストール

ソース: b90f807be10d0c

npmでさくっと入ります.
frontend/ ディレクトリに移動し,
$ npm install --save vuex とコマンド実行してください.

vuexソースコードの記述

ソース: be10d0c0c90ee7

慣例的に store としているのでそれに合わせます.

frontend/src/ 以下に storeディレクトリを作りましょう.
公式のサンプルではactionやmutation用にjsファイルを作ってますが,
今回はとりあえず index.js に全部いれちゃいます.
最初なのでまずはログインしているかどうかだけ保持し,状態を切り替えるためのactionとmutationを用意します.

frontend/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

const state = {
  isLoggedIn: false,
}

const mutations = {
  loggedIn (state) {
    state.isLoggedIn = true
  },
  loggedOut (state) {
    state.isLoggedIn = false
  },
}

const actions = {
  login ({commit}) {
    commit('loggedIn')
  },
  logout ({commit}) {
    commit('loggedOut')
  },
}

const getters = {
  isLoggedIn: state => state.isLoggedIn,
}

export default new Vuex.Store({
  strict: debug,
  actions,
  getters,
  mutations,
  state,
})

ここまで書けたら main.js を修正し,Vueにセットします.

frontend/src/main.js
...
import store from './store'  // <----- 追加
import api from './api'

...
new Vue({
  el: '#app',
  store,  // <----- 追加
  router,
  components: { App },
  template: '<App/>',
})

このままだと動作がわからないので適当な場所に入れて動作確認しましょう.
今回は App.vue に書いていきます
template内にはログイン中かどうかの情報を表示させます.
また,ログイン用,ログアウト用のボタンを配置します.

vuex には mapActionsmapGetters というヘルパーが用意されています.
これをそれぞれmethods, computedに記述することでメソッドや算術プロパティとして登録できます.

frontend/src/App.vue
<template>
  <v-app id="app">
    <v-navigation-drawer app></v-navigation-drawer>
    <v-toolbar app>
      {{ isLoggedIn ? 'ログイン中' : 'ログアウト中' }}
      <v-btn @click="login">ログイン</v-btn>
      <v-btn @click="logout">ログアウト</v-btn>
    </v-toolbar>
    <v-content>
      <v-container fluid>
        <router-view></router-view>
      </v-container>
    </v-content>
    <v-footer app></v-footer>
  </v-app>
</template>

<script>
import {mapActions, mapGetters} from 'vuex'

export default {
  name: 'App',
  computed: {
    ...mapGetters(['isLoggedIn']),
  },
  methods: {
    ...mapActions(['login', 'logout']),
  },
}
</script>

ブラウザで開いて動作確認しましょう.
今回はルートコンポーネントに差し込んだのでトップページでもログイン画面でもヘッダの位置に文字とボタンが表示されます.

Kobito.91O0Bt.png

ログイン処理の記述

ソース: 0c90ee7ae560d3

ボタンを押すことでvuexのstate変更ができるようになりましたが,
この状態では本当のログイン操作は行えていません.
store/index.js のloginアクションを拡張して実際のログイン処理を記述します.
現状ではLogin.vue内でログインボタン押下時に this.$request からAPIを直接叩いていましたが,
ログイン状態のvuex側で保持する必要があるのでAPIの呼び出しをvuexのactionに移動させます.

以下修正した部分の抜粋です.

frontend/src/store/index.js
import Vuex from 'vuex'

import client from '@/api' // 追加

...

const mutations = {
  loggedIn (state, token) {  // 引数追加
    state.isLoggedIn = true
    client.defaults.headers.common['Authorization'] = `JWT ${token}`  // ここでtokenセット
  },
...
const actions = {
  login ({commit}, [username, password]) {  // 引数追加,配列で受け取るので展開して受け取る
    return client.auth.login(username, password).then(res => {
      commit('loggedIn', res.data.token)
      return res
    })
  },

Login.vueでやっていたAPIの呼び出しや成功時の動作の一部をstoreに移動させただけです.
Vueコンポーネント内では this.$request で参照していましたが,store内では client としてimportしてます.

actionsのloginメソッドの第2引数ですが,ここはvuexでmethodをmapする際に2つ以上の引数が上手く渡せないので
呼び出し元で配列で渡すようにしています.そのため,ここでは受け取った配列を展開するように記述しています.

続いてLogin.vue側です.

frontend/src/components/Login.vue
<script>
import {mapActions} from 'vuex'  // 追加

export default {
  name: 'Login',
...
  methods: {
    ...mapActions(['login']),
    submit () {
      this.nonFieldErrors = []
      this.login([this.username, this.password]).then(res => {
        this.$router.push('/')
      }, err => {
        this.nonFieldErrors = err.response.data.nonFieldErrors
      })
    },
  },
}

こちらはscriptの頭で mapActions をimportしてloginアクションを使えるようにしています.
this.$requestの時もアクションのloginの時の戻り値はPromiseなので記述自体は殆ど変わりません.
tokenのセット処理をstore内に移動させているのでその部分だけ削除しています.
this.loginの引数が配列になっている点に注意してください.

ログアウト処理とAppの修正

ソース: ae560d342f7864

ログイン処理を入れたので次はログアウト処理です.
ログイン時にはヘッダにtokenをセットしましたが,ログアウト時にはこれを消してあげましょう.
この後にページ遷移させたいならコンポーネント側に処理が必要ですが,取り敢えず今回はこれで.

frontend/src/store/index.js
const mutations = {
...
  loggedOut (state) {
    state.isLoggedIn = false
    delete client.defaults.headers.common['Authorization']
  },
}

ついでにApp.vueで記述したテスト用のヘッダも修正します.
今は常にログイン/ログアウトボタンがついていますが,ログアウト時にはログイン画面へのリンク,
ログイン時にはログアウトボタンを表示するようにします.

frontend/src/App.vue
    <v-toolbar app>
      <router-link v-show="!isLoggedIn" :to="{name: 'Login'}">ログイン</router-link>
      <v-btn v-show="isLoggedIn" @click="logout">ログアウト</v-btn>
    </v-toolbar>

もうかなり慣れてきたと思いますが,コンポーネントの表示/非表示は v-show を使います.

v-if でもいいです.
v-showの場合,非表示時にもDOMは存在するが,表示はされていない,という状態になります.
v-ifの場合は非表示時にはDOM自体が存在せず,表示状態になった時に作成されます.
公式を参考にどちらを使うか考えてみてください.

<router-link> は vue-routerが持っているコンポーネントです.
toとしてnameを渡すと router/index.js で定義した名前のコンポーネントからURLが逆引きできます.

ログイン情報の復元

ソース: 42f7864c64c47c

今の状態だとブラウザのF5を押すとログアウト状態になってしまいます.
これはコンポーネントやvuexが持つデータはどこにも保存されず,javascript再読込時に初期化されるからです.
これを防ぐためには,ログイン成功時に取得したtokenをcookieかlocalStorageに保存する必要があります.
今回はlocalStorageに保存することにします.

Qiita: sessionとcookieとWeb Storageの違い

まずはstoreのmutationを修正し,ログイン時にlocalStorageに保存,ログアウト時にlocalStorageから削除する処理を挟みます.
また,localStorageからの復元を試みるactionを追加します.

frontend/src/store/index.js
const mutations = {
  loggedIn (state, token) {
    state.isLoggedIn = true
    client.defaults.headers.common['Authorization'] = `JWT ${token}`
    localStorage.setItem('token', token)  // <---- 追加
  },
  loggedOut (state) {
    state.isLoggedIn = false
    delete client.defaults.headers.common['Authorization']
    localStorage.clear()  // <--- 追加
  },
}

const actions = {
...
  tryLoggedIn ({commit}) {
    const token = localStorage.getItem('token')
    if (token) {
      commit('loggedIn', token)
    }
  },
}

storeの修正ができたら今度は呼び出し処理です.
今回は一番TOPのコンポーネントであるApp.vueが作られるタイミングでローカルストレージからの復元処理を呼び出すようにします.
修正は以下のようになります.

frontend/src/App.vue
   created () {  // イベント登録
     this.tryLoggedIn()
   },
   methods: {
   ...mapActions(['login', 'logout', 'tryLoggedIn']),  // 'tryLoggedIn' 追加
   },

ここまでできたらブラウザで一度ログインし,ブラウザを更新してみましょう.
更新後もログイン状態が保持されていれば(ログアウトボタンがヘッダに表示されていれば)成功です.

Kobito.uuzS7y.png

tokenの検証処理を入れる

今はtokenがlocalStorageに保存されていればログイン状態になってしまいます.
しかし,tokenの有効期限が切れていたり,ユーザそのものが削除されていたりし,本当はログイン状態ではない可能性も考えられます.
そこで,localStorageからtokenを復元する際に検証を行い,tokenが正しい場合のみログイン処理を行うように修正します.

token検証APIの追加

ソース: c64c47c25731fa

まずはdjango側に手を入れます.
token発行用のAPIを作った時のように,token検証用のviewもすでにライブラリで提供されています.
従って,やることはurls.pyの記述だけです.

tutorial/urls.py
from rest_framework_jwt.views import obtain_jwt_token
from rest_framework_jwt.views import verify_jwt_token  # <--- 追加
...
api_urlpatterns = [
    path('auth/', obtain_jwt_token),
    path('auth/verify/', verify_jwt_token),  # <--- 追加
    path('questions/', include(question_router.urls)),
...

これでtokenをpostして正しいかどうか調べるための準備ができました.
不正な値を入れると下記のようにステータスコード400が返ります.

Kobito.hikEHo.png

検証用APIの呼び出し処理追加

ソース: 25731fade22002

まずクライアントから呼び出せるように検証用APIを追加します.

frontend/src/api/auth.js
export default function (cli) {
  return {
...
    verify (token) {
      return cli.post('auth/verify/', {token})
    },
  }
}

次に,storeのtryLoggedInアクションを修正し,APIの戻り値が正しい場合のみログイン処理を行うことにします.
APIの戻り値が正しくない場合,無効なtokenがlocalStorageに残ってることになるので削除しておきます.

frontend/src/store/index.js
  tryLoggedIn ({commit}) {
    const token = localStorage.getItem('token')
    if (token) {
      client.auth.verify(token).then(() => {
        commit('loggedIn', token)
      }, () => {
        // 不正なtoken
        localStorage.clear()
      })
    }
  },

これで復元時にtokenの検証を行う処理完了です.

次回はログインしないと入れないページを作っていきます.

チュートリアルまとめ

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした