Vuexを使って

いい感じにjwt認証を実装してみた

2019/09/18 v-kansai Vue.js/Nuxt Meetup #10

Motoyasu Meguro

自己紹介

  • 目黒 元康(1974年12月生まれ)
  • 19年のサラリーマンSE生活を退職しフリーランスになる。
  • 現在株式会社@IoT (滋賀県草津市) に所属しつつ。
  • CODE-HOUSE(個人事業)を経営中。

趣味は自転車

@mm-git

@wRoute

株式会社 @IoT

滋賀県草津市

主な業務内容

  • IoTアプリ開発(sigfox/awsを使ってIoTの情報を可視化)
  • go/postgresqlを使ったバックエンド開発
  • aws/GCPのサーバインフラ整備
  • vue/reactを使った業務系アプリ開発
  • iOS/androidアプリ開発

CODE HOUSE

I'm a software engineer.

 

  • 主に業務系アプリ開発
  • サーバインフラ、バックエンド、フロントエンドまで
  • vue/go(graphQL)
  • iOSアプリリリースあり

 (mapboxを使った地図アプリです)

Route GPXコマ地図エディタ

App storeにて販売中

本題

 サンプルアプリ

ログインしたらTODOリストが見れるアプリ

を例にお話しします。

話すこと

  • Vuexを使う理由
  • jwtを使ったvuex実装例サンプル

話さないこと

  • Vuexのテスト関連
  • typescript関連

本日のコード

  • vuex-jwt-sample(フロント)

https://github.com/mm-git/vuex-jwt-sample

 

  • auth-sample-jwt(バックエンド)

https://github.com/mm-git/auth-sample-jwt

本題の前に

Vuexとは?

Vuex

  • vueで扱いたい、状態、データをコンポーネントとは別にパッケージ化してくれるライブラリ。

 

  • コンポーネントからはVuexのActionsを実行する。
  • Vuexでバックエンドとのやり取りを行い、結果をMutationsを介してStateに反映する。
  • コンポーネントはStateの値を表示に使う。

出典 [https://vuex.vuejs.org/ja/] vuexとは何か

本題の前に

jwt(json web tokenとは)

jwtとは

  • ユーザ情報(userIdなど)や有効期限を含んだjson文字列に電子署名をして改ざんできないようにしたもの。
  • URL-safeな文字列(base64)。
  • 実際のtoken文字列↓

ログインとjwt

  • 最初だけIDとパスワードでログイン認証を行う。
  • ID、パスワードが合っていた場合のみjwtが発行される。
  • jwtの有効期限が切れるまではjwtを使ってサーバにアクセスできる。

ID, password

jwt

※ホテルのチェックインで名前を言うと、チェックアウトまで有効なカードキーをもらうようなイメージ

jwtの中身

ピリオドで3パートに区切られている

  • header(暗号の方式など)
  • payload(ユーザ情報や有効期限)
  • signature(電子署名)

jwtのpayload

{
    "uid":"test",
    "exp":1568042806,
    "jti":"d04a53b6-1087-4c14-b0b1-bd47c78a8d47",
    "iat":1568042786,
    "iss":"auth-sample-jwt",
    "sub":"AccessToken"
}

↓base64デコード

ユーザID

有効期限

2種類のトークン

アクセストークン

  • APIのアクセスに使うトークン
  • 有効期限が短め(1時間など)
    • リフレッシュのないサービスだと1時間後にはログアウト
  • ​有効期限内は何回でも使える。

リフレッシュトークン

  • アクセストークンの有効期限更新に使うトークン
  • 有効期限が長め(24時間とか、半年とか)
  • 一回のみリフレッシュできる。
  • リフレッシュすると、それまでのアクセストークンは無効

このようなjwtを

vuexを使っていい感じに管理しよう

・・・というのが今日のお話

なぜVuexを使うのか?

Vuexを使う理由①

  • 複数のページで使うデータを一元管理
  • 他のVuexストアから使うデータを一元管理

複数個所で使うデータを一元管理したい

今回の例だとログインしているのかの状態とjwtのデータを1つのストアで管理したい。

Vuexを使う理由②

  • コンポーネント(SFC)は表示にかかわることのみを書きたい。
  • Vuex側に表示以外のコードを分離したい。
  • 特にサーバとの非同期な処理をSFCに書きたくない。

コンポーネント側にバックエンドとのやり取りを書きたくない

ログイン認証の処理をSFCには書きたくない。

Vuexを使う理由③

devtoolsで状態を確認しやすい

  • ストアごとの状態をdevtoolsでパッと見ることができる。

Vuexの実装例

ログインストア実装

ログインを管理するauthストアを実装してみる

ストアフォルダの構成

  • Vuexのstoreごとにフォルダをわける。
  • 各ストアはstateやactionごとにファイルをわける。
  • アプリの規模が大きくなるようなら、更にサブフォルダにわける。

 

・・とわかりやすいです。

Tips !!

ちなみに、ストアのindex.jsの中身は・・

stateやactionsのファイルをimportして

それらをexport defaultしているだけ

store.jsなどの中身は・・

export defaultにstateやactionsの中身を書いていく

ストアの持つデータ

  • サーバから直接受け取るデータ + そのストアの状態を表すデータはstate.jsに記載
  • これらのデータから算出できる情報はgettersに記載する。

Tips !!

state.js

  • ログイン成功時にサーバから受け取るデータ
    • accessToken / refreshToken
  • authストアの状態
    • status

getters.js

ログインしているかどうか

トークンのpayload

トークンの有効期限

stateから算出できるものはgettersに記載。

ログインの状態遷移

  • そのストアの持つ状態とその遷移のパターンを考えてみる。
  • 状態をそのままmutationsに実装するときれいにまとまる。

Tips !!

mutation-types.js

mutaionsで定義する関数の名前(=状態)は、

あらかじめconstで定義しておくと便利。

mutations.js

mutationsの各関数は、mutation-types.jsで定義したconstを使って書いていく。

['文字列']で関数が定義できるのが、最初理解できませんでした・・・

遷移の矢印をactionsで実装

  • 実装が必要なのは action と書いてある矢印。
  • それ以外の矢印は、actionの結果非同期で遷移が発生する。

actions.js の login()

  • actionsの関数は非同期な処理なので、asyncで定義する。
  • 非同期処理の前にrequesting状態にする。(commit)
  • サーバへの非同期リクエストをawaitで待って、結果をcommitする。
    • ​成功:login状態へ
    • 失敗:logoutへ戻る
  • 最終状態がloginならtrueを応答する。

Tips !!

actions.js の logout()

  • 処理としては、ストアの持っているデータをクリアするだけ。
  • なので非同期な処理はなく、logout状態をcommitするだけ。

actions.js の refresh()

  • トークンの有効期限が十分なら、何もせずtrueを返す。
  • 非同期処理の前にrefreshing状態にする。(commit)
  • サーバへの非同期リクエストをawaitで待って、結果をcommitする。
    • ​成功:login状態へ
    • 失敗:expiredへ
  • 最終状態がloginならtrueを応答する。

store.jsの編集とストアの永続化

  • 追加したVuexストアは自動では読み込んでくれない。
    • store.jsの編集が必要。
  • ブラウザのリロードでauthのデータがクリアされると困る。
    • vuex-persistedstateの導入。

Tips !!

src/store.js

Vuex.storeのmodulesに

作ったストアを追加していく

vuex-persistedstateプラグイン

永続化したいストア全体または

ストアのstateを追加する。

ログイン画面実装

ログイン画面からauthストアを使ってみる

ログイン画面

Vuexにログイン処理のほとんどが書かれているので

 

  • ログイン画面ファイルのほとんどが、画面デザインに関するコード
  • loginボタンを押したら、Vuex authストアのlogin()アクションを呼ぶだけでいい。

views/Login.vue

VuexのmapXXXXヘルパ関数

ストアのactionやstateなどを、あたかもこのコンポーネントに定義したmethodsやpropsの様に扱うことができる。

Tips !!

のかわりに

this.$store.dispatch('auth/login', ...)
this.login()

だけでストアのactionを実行することができる。

views/Login.vue

ストアのlogin()の結果をawaitで待つことで、ログインの成功/失敗を判定できる。

Tips !!

awaitでログイン結果が待てるので、watch:でauthのstatusを監視する様なことは不要。

TODO画面実装

ログイン後の画面はauthストアを意識しない

TODO画面

TODOのリストをサーバから取得する処理も、Vuexに分離する

 

  • TODO画面は、TODOリスト取得に必要なアクセストークンを知る必要はない。
  • 画面が表示されたら、VuexのTODOリスト取得アクションを呼ぶだけ。
  • 取得されたTODOリストをVuexのStateを介して表示

views/List.vue

TODOリストのコンポーネントの処理はこれだけ!!

  methods: {
    ...mapActions('todoList', ['fetch']),
  },
  computed: {
    ...mapState('todoList', ['todoList'])
  },
  async created () {
    await this.fetch()
  },

todoListのactions.js

  • アクションから他のストアのアクションを呼ぶことができる。
  • 他のストアのStateを参照することもできる。
import Vue from 'vue'
import * as types from './mutation-types'

export default {
  async fetch (action) {
    const res = await action.dispatch('auth/refresh', {}, { root: true })
    if (!res) {
      return false
    }

    action.commit(types.REQUESTING)
    
    const accessToken = action.rootState.auth.accessToken
    //以下 axiosを使ったGET処理
    :
  }
}

Tips !!

トークンが必要な処理の直前でauthのrefresh()を実行

authストアの

アクセストークンを使用

デモ

ログインアプリを動かしてみます

  • ID: test、password: aaaaaaaa
  • アクセストークン15秒
  • リフレッシュトークン20秒

その他のTips

Router.js

すでにログイン済みの状態でログイン画面にアクセスした場合。

Tips !!

:
import store from './store'

Vue.use(Router)

const guardLogin = (to, from, next) => {
  if (!store.getters['auth/isAuthenticated']) {
    next()
    return
  }
  next('/list')
}

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'login',
      component: Login,
      beforeEnter: guardLogin
    },
    :
  ]
}
                          

beforeEnterでTODOリスト画面へ移動させることができます。

・・・もちろんその逆も。

詳細はコード参照。

App.vue

トークンの有効期限がきれたら、timeout画面へ移動

Tips !!

export default {
  name: 'app',
  computed: {
    ...mapState('auth', ['status'])
  },
  watch: {
    status (newStatus) {
      if (newStatus === 'token_expired') {
        this.$router.push('/timeout')
      }
    }
  }
}

おしまい

ありがとうございました

Made with Slides.com