【JWT】もう怖くない、JWT認証(Vue.js編)

はじめに

この記事は、【JWT】もう怖くない、JWT認証(RailsAPI編)の続編です。
まだ読まれていない方は、こちらからどうぞ!

bon-voyage23.hatenablog.com

今回は、Vue(client)側の設定をやっていきます!

JWT認証の流れ(Vue.jsとRails

前回の記事では、Rails(server)側の設定を行いました!
下の図でいうと、赤色の部分ですね。
今回は、Vue.js側の緑色の部分の内容です。

f:id:voyag:20210326173816p:plain
JWT

環境

既存のRailsにVue.jsを導入

RailsにVue.jsを導入する方法としては、大きく分けると以下の2つあるかと思います。

  1. webpackオプションを使って、Rails newをする方法

  2. 既存のRailsrails webpacker:install:vueをして導入する方法

今回は、すでにRails newをしてしまっているので、後者の方法をとります。
ちなみに前者は、こちらの記事で簡単にやり方を記載していますので、
気になる方はcheck it out!! bon-voyage23.hatenablog.com

インストール

Gem webpackerを追加。

gem 'webpacker', github: 'rails/webpacker'

yarnもインストール。

brew install yarn

webpacker 初期化。

rails webpacker:install

vueインストール。

rails webpacker:install:vue

これで、vueの埋め込みが完了!

Hello Vueする

Vueの埋め込みができたので、次は「Hello Vue」を表示させていきます。

前回、routes.rbでrootをhome#indexに設定したので、Homeコントローラを作成していきます。

rails g controller home

続いて、Homeコントローラにindexメソッドを追加。

class HomeController < ApplicationController
  def index; end
end

最後にhomeビューでjavascript_pack_tag を設定。

<%= javascript_pack_tag 'hello_vue' %>

これで、rails sすると、先程インストールしたVue.jsのapp.vueを読みに行ってくれます!
下のように表示されればOK!!

Image from Gyazo

トップページ

最初にトップページを作成していきます!

ライブラリの追加

ユーザー登録の処理には、axios,vue-router,vuex,を使用しますので、
yarnを使ってライブラリを追加しておきます。
(vuetifyはせっかく作るならちょっとは見た目を整えたいという私個人の問題なので、ただ機能だけ実装したいのであれば入れなくて大丈夫です!)

yarn add axios
yarn add vue-router
yarn add vuex
yarn add vuetify

インストールができたら、hello_vue.jsにこのライブラリ使うよ〜って言っといてあげましょう!

import Vue from 'vue'
import vuetify from '../plugins/vuetify'
import App from '../app.vue'
import router from '../router'
import store from '../store'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    router,
    vuetify,
    store,
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)

  console.log(app)
})

vuetifyに関する設定は、pluginとして定義してあげます。
テーマをlightにしてますが、darkにするとダークモードになります(当たり前)。

vuetifyjs.com

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  theme: { light: true },
})
ルーティングの設定

次にVue-routerのルーティングを設定していきます。
ユーザー登録ページのコンポーネント名をSignupとして作成するので、このようになります。
(ついでにトップページとログインページも設定してます。)

import Vue from "vue";
import Router from "vue-router";
import Top from "../pages/Top";
import Signup from "../pages/Signup";
import Login from "../pages/Login";

Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Top',
      component: Top,
    },
    {
      path: '/signup',
      name: 'Signup',
      component: Signup,
    },
    {
      path: '/login',
      name: 'Login',
      component: Login,
    }
  ],
})

export default router
トップページの表示

まずはrouterで設定したページを表示させるために、app.vueを修正しておきます。

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

トップページには簡単にログインボタンと新規登録ボタンを置いておきます。

<template>
  <div>
    <v-app>
      <v-card width="400px" class="mx-auto mt-5">
        <v-card-title>
          <h1 class="display-1">JWT認証SampleApp</h1>
        </v-card-title>
      </v-card>
      <v-row  justify="center" align-content="center">
        <v-img src="https://cdn.vuetifyjs.com/images/parallax/material2.jpg">
          <div class="fill-height bottom-gradient"></div>
        </v-img>
      </v-row>
      <v-row  justify="center" align-content="center">
        <v-spacer />
        <v-btn
          to="/login"
          class="btn btn-dark mt-5"
        >
          ログイン
        </v-btn>
        <v-spacer />
        <v-btn
          to="/signup"
          class="btn btn-dark mt-5"
        >
          新規登録
        </v-btn>
        <v-spacer />
      </v-row>
    </v-app>
  </div>
</template>

<script>
export default {
  name: "Top"
}
</script>

こんな感じになります。 (なんか寂しかったんで、画像入れました←余計) Image from Gyazo

ユーザー登録

トップページができたので、ユーザー登録フォームとその処理を設定していきます!

ユーザー登録フォーム

まずは見た目から。笑

<template>
<div>
  <v-app>
    <v-card width="400px" class="mx-auto mt-5">
      <v-card-title>
        <h1 class="display-1">ユーザー登録</h1>
      </v-card-title>
      <v-card-text>
        <v-form>
          <v-text-field 
            label="ユーザ名"
            v-model="user.name" 
          />
          <v-text-field 
            type="email"
            label="メールアドレス"
            v-model="user.email" 
          />
          <v-text-field 
            type="password"
            label="パスワード"
            v-model="user.password"
          />
          <v-text-field 
            type="password"
            label="パスワード(確認)"
            v-model="user.password_confirmation"
          />
        </v-form>
          <v-card-actions>
            <v-btn class="info" @click="signup">登録</v-btn> 
          </v-card-actions>
      </v-card-text>
    </v-card>
  </v-app>
</div>
</template>

<script>
export default {
  name: "Signup",
  data() {
    return {
      user: {
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
      }
    }
  },
  methods:{
    submit(){
      console.log(this.user.name,this.user.email,this.user.password,this.user.password_confirmation)
    }
  }
};
</script>

これでこんな感じの登録フォームができるかと思います。
今は登録ボタンを押しても、ただコンソールに入力した情報が表示されるだけになっています。

Image from Gyazo

axiosの設定
import axios from 'axios';

const token = document.querySelector('meta[name="csrf-token"]').content;
const axiosInstance = axios.create({ 
  baseURL: '/api/v1',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': token
  },
  responseType: 'json'
});
export default axiosInstance
  • tokencsrf-tokenを取得しています
  • headersで取得したcsrf-tokenを標準でheadersに仕込むようにしています

hello_vue.jsに以下を追記しておきます。

import axios from '../plugins/axios'

Vue.prototype.$axios = axios
signup

axiosの設定ができたので、signupの処理を追記していきます。

.
.
.
  methods: {
    signup() {
      this.$axios.post('users', { user: this.user })
        .then(res => {
          this.$router.push({ name: 'Login' })
        })
        .catch(err => {
          console.log(err)
        })
    }
  }
}
</script>
  • users_controllerのcreateメソッドを呼ぶように、postします
  • 問題なく登録できた場合は、$route.pushでLoginページに遷移するようにしています

ログイン

ユーザー登録ができたので、ログインの処理をしていきます。

ログインフォーム

このような形で実装しました!
見た目はSignup.vueを流用してます。

<template>
<div>
  <v-app>
    <v-card width="400px" class="mx-auto mt-5">
      <v-card-title>
        <h1 class="display-1">ログイン</h1>
      </v-card-title>
      <v-card-text>
        <v-form>
          <v-text-field 
            label="メールアドレス"
            v-model="user.email" 
          />
          <v-text-field 
            type="password"
            label="パスワード"
            v-model="user.password"
          />
        </v-form>
          <v-card-actions>
            <v-btn class="info"
              @click="login" 
              :disabled="handleIsDisabled"
            >ログイン</v-btn> 
          </v-card-actions>
      </v-card-text>
    </v-card>
  </v-app>
</div>
</template>

<script>
import { mapActions } from "vuex"

export default {
  name: "Login",
  data() {
    return {
      user: {
        email: '',
        password: ''
      }
    }
  },
  computed: {
    handleIsDisabled() {
      if(this.user.name !== '' && this.user.email !== '') { return false } else { return true }
    }
  },
  methods: {
    ...mapActions("users", [
      "loginUser",
    ]),
    async login() {
      try {
        await this.loginUser(this.user);
        this.$router.push({ name: 'Top' })
      } catch (error) {
        console.log(error)
        alert('ログインに失敗しました')
      }
    },
  }
}
</script>

<style lang="scss" scoped>
</style>
  • ログインボタンを押したときに、loginメソッドが走るようになっており、loginUserはstoreのuserモジュールで定義するmapアクションです
  • ログインが成功した際には、Topページに遷移するようにしています(ほんとはログインの制約をかけたページに遷移するべきなんですが、、)
  • ログインが失敗したときは、alertが表示されます
Cookieの設定

今回は、JWTtokenをCookieに格納しておきたいと思います。 そのために、vuex-persistatestorejs-cookieライブラリを追加しておきます。

yarn add vuex-persistatestore
yarn add js-cookie

vuex-persistatestoreの使い方は、こちらの記事にもまとめているので、なんじゃそりゃって方はどうぞ!

bon-voyage23.hatenablog.com

js-cookieは公式を参照のこと。

github.com

userモジュール

今回はストアをモジュールに分けています。
userしか無いのに分けてるのは、userしかないアプリを作らんやろっていう理由です。はい。

import axios from '../../plugins/axios'
import createPersistedState from 'vuex-persistedstate';
import Cookies from 'js-cookie';

const state = {
  loginUser: null
}

const getters = {
  loginUser: state => state.loginUser
}

const mutations = {
  setUser: (state, user) => {
    state.loginUser = user
  }
}

const actions = {
  async loginUser({ commit }, user) {
    const sessionsResponse = await axios.post('sessions', user)
    const auth_token = sessionsResponse.data.token
    Cookies.set('token', auth_token, 1)
    axios.defaults.headers.common['Authorization'] = `Bearer ${auth_token}`
    
    const userResponse = await axios.get('users/info')
    commit('setUser', userResponse.data)
    }
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
  plugins: [createPersistedState({
    storage: {
      getItem: key => Cookies.get(key),
      setItem: (key, value) => Cookies.set(key, value, { expires: 3, secure: true }),
      removeItem: key => Cookies.remove(key)
    }
  })],
}

--loginUserの処理の流れ--

  1. const sessionsResponse = await axios.post('sessions', user) sessions_controllerのcreateメソッドから帰ってきたtokenをsessionsResponseに格納

  2. const auth_token = sessionsResponse.data.token sessionsResponseからtokenを取り出して、auth_tokenに格納

  3. Cookies.set('token', auth_token, 1) cookieの中にtokenという箱を作って、その中にauth_tokenを格納。1は保存日数を表しています。

  4. axios.defaults.headers.common['Authorization'] =Bearer ${auth_token}`` axiosを用いて、Authorizationリクエストヘッダにauth_token付与して、サーバ側に送信できるようにする。

  5. const userResponse = await axios.get('users/info') users_controllerのinfoメソッドにgetリクエストを送って、user情報を取得し、その結果をuserResponseに格納。

これでログイン処理の完了です!

Image from Gyazo

トップページに戻ってくるので、ログインした感じしないですが、ちゃんと戻って来れているので、成功です!笑

おわりに

本当はbeforeEachで認証が必要なページへの画面遷移の制御とかもやりたかったんですが、そこまですると蛇足感があったので、省きました。
押さえたかったのはログイン時のJWTtokenがどうやって生成されて、どう行き来するのかというところだったので。
コードの部分が多く読みづらい記事になってしまいましたが、誰かの役に立てれば!!

今回作成したサンプルアプリのコードはGithubに置いてますので、ご参考までに。。

github.com

参考

当サイトは、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイトプログラムである、Amazonアソシエイト・プログラムの参加者です。