バックエンド側の認証制御は実装できたので
次はフロントエンドにログイン機能を実装していきます。
vuexを入れてユーザ情報を一元管理する
vuexとは、vue-routerと同じくVue.jsのオプションパッケージで
SPAアプリケーション全体で保持する状態やデータを管理するライブラリです。
たとえば全コンポーネントから参照されるログイン中のユーザ情報などを扱うのに適しています。
$ npm install vuex --save-dev
npmでインストールします。
resources/assets/js/store/ディレクトリを作り、以下の3ファイルを作成します。
import http from "../services/http"; const Vuex = require('vuex'); const {MUTATION} = require('./mutation-types'); const {ACTION} = require('./action-types'); const state = { me: null, }; const getters = { me: (state) => { return state.me; } }; const actions = { [ACTION.LOGIN]({commit}, {email, password, successCb, errorCb}) { let loginParams = {email: email, password: password} http.post('authenticate', loginParams, res => { commit(MUTATION.SET_ME, {me: res.data.user}); successCb(res); }, error => { errorCb(error); }); }, [ACTION.LOGOUT]({commit}, {successCb, errorCb}) { http.get('logout', () => { successCb(); }, error => { errorCb(error); }); }, [ACTION.ME]({commit}, {successCb, errorCb}) { http.get('me', res => { commit(MUTATION.SET_ME, {me: res.data.user}); successCb(res); }, error => { errorCb(error); }); } }; const mutations = { [MUTATION.SET_ME](state, {me, token}) { state.me = me; console.log('MUTATION.SET_ME', this.state); }, [MUTATION.REMOVE_ME](state) { state.me = null; localStorage.removeItem('jwt-token'); console.log('MUTATION.REMOVE_ME', this.state); } }; export default new Vuex.Store({ state, getters, actions, mutations });
export const LOGIN = 'LOGIN'; export const LOGOUT = 'LOGOUT'; export const ME = 'ME'; export const ACTION = { LOGIN, LOGOUT, ME };
export const SET_ME = 'SET_ME'; export const REMOVE_ME = 'REMOVE_ME'; export const MUTATION = { SET_ME, REMOVE_ME };
index.js内の「state」が保持するデータ、「getters」がその名の通りデータのgetterです。
「actions」が各コンポーネントから呼び出されるメソッドになりますが、
「mutations」だけがstateを変更できるという構造になっています。
こうすることで、stateデータは必ずこの処理を通して扱われる = 一元管理されることとなります。
これをStoreパターンと呼びます。
今回は認証処理とそこで取得されるログインユーザ情報をStoreで管理する形になります。
ログインアクションが成功したらユーザ情報をミューテーションでstateに記憶し、
ログアウトしたらその逆、といった実装になっています。
ログイン画面 / ログアウト処理
<template> <div> <h1>Login</h1> <div v-if="showAlert" style="color: #FF9999;">{{ alertMessage }}</div> <label for="email">E-Mail Address</label> <div> <input id="email" type="email" v-model="email" @keyup.enter="login" required autofocus> </div> <label for="password">Password</label> <div> <input id="password" type="password" v-model="password" @keyup.enter="login" required autofocus> </div> <button @click="login">Login</button> </div> </template> <script> const store = require('../store/').default; const {LOGIN} = require('../store/action-types'); export default { mounted() { }, data() { return { email: '', password: '', showAlert: false, alertMessage: '', } }, methods: { login() { let params = { email: this.email, password: this.password, successCb: res => { console.log('logged in.', res); this.$router.push('/users/') }, errorCb: error => { this.showAlert = true; this.alertMessage = 'Wrong email or password.' } }; store.dispatch(LOGIN, params); }, } } </script>
vuexで作成したstoreを使ってログインアクションをdispatchします。
ログインに成功したら/users/にリダイレクト。
<template> </template> <script> const store = require('../store/').default; const {LOGOUT} = require('../store/action-types'); export default { mounted() { let params = { successCb: res => { console.log('logged out.'); this.$router.push('/login') }, errorCb: error => { console.log(error); } }; store.dispatch(LOGOUT, params); }, data() { return { } }, methods: { } } </script>
こちらはログアウト処理。
コンポーネントにする必要はないのですが、
わかりやすいので空のコンポーネントにログアウト処理を書いて
最後にログイン画面にリダイレクトしています。
ルーティング
Vue.use(require('vuex')); const {ME} = require('./store/action-types'); ・・・ const store = require('./store/').default; const router = new VueRouter({ mode: 'history', routes: [ { path: '/users', component: require('./components/Users.vue'), children: [ {path: '/users/', component: require('./components/Users/Index.vue'), meta: {requiredAuth: true}}, {path: '/users/edit/', component: require('./components/Users/Edit.vue'), meta: {requiredAuth: true}}, {path: '/users/edit/:id', component: require('./components/Users/Edit.vue'), meta: {requiredAuth: true}}, ], meta: {requiredAuth: true} }, {path: '/login', component: require('./components/Login.vue')}, {path: '/logout', component: require('./components/Logout.vue')}, ] }); router.beforeEach ((to, from, next) => { if (!store.getters.me) { http.init(); store.dispatch(ME, { successCb: res => { console.log('me res', res); next(); }, errorCb: error => { console.log('me error', error); if (to.matched.some(record => record.meta.requiredAuth)) { next({path: '/login', query: {redirect: to.fullPath}}); } else { next(); } } }); } else { next(); } }); const app = new Vue({ router, store, created() { http.init(); }, el: '#app' });
/loginと/logoutをルーティングに追加。
router.beforeEachは新しいルートに遷移したすべてのタイミングで走る処理を登録する関数です。
ここで、storeにログインユーザ情報があるか確認し、なければ/meに問い合わせて
トークンが生きているか(ログイン中か)を画面遷移の度に確認します。
どの画面を要認証とするかは、routes設定のmeta.requiredAuthで指定します。
リクエストヘッダーにJWTトークンを付与
export default { isInited: false, ・・・ /** * Init the service. */ init () { if (this.isInited) return; this.isInited = true; axios.defaults.baseURL = '/api' // Intercept the request to make sure the token is injected into the header. axios.interceptors.request.use(config => { config.headers['Authorization'] = `Bearer ${localStorage.getItem('jwt-token')}`; return config }) // Intercept the response and ... axios.interceptors.response.use(response => { // ...get the token from the header or response data if exists, and save it. const token = response.headers['Authorization'] || response.data['token']; if (token) { localStorage.setItem('jwt-token', token) } return response }, error => { // Also, if we receive a Bad Request / Unauthorized error console.log(error) return Promise.reject(error) }) } }
APIへのリクエストを一括管理しているhttpサービスを上記のように変更します。
レスポンスにjwtトークンが含まれていればローカルストレージに保存し、
リクエスト時にそのトークンを自動付与してAPIをコールする仕組みです。
これでフロントからログインしてAPIを利用できるようになりました。