axiosを使ってJWT認証を行う

##はじめに
axiosを使って自分のAPIサーバーとデータのやり取りを行う上でJWT認証トークンについて述べられている文献が少ないと思ったのでここで載せておきます。フロントエンド開発の負担軽減になれば幸いです。ついでにaxiosについても少し解説します。
##使うもの
* Python 3.82
* Django 2.1
* Django-rest-framework 3.12.4
* Npm 6.14.11
* Vue.js 2.6
* Vuex 3.6
* Axios

上三つはバックエンド用です。今回はaxiosの解説なので説明しません。Vue.jsはjavascriptフレームワークで、Reactでも多分大丈夫です。Vuexは状態管理用のツールで、ユーザーのTokenをローカルストレージに保存したりそれを変更したりするときに使います。

##Axiosとは
Webサイトなどを構築する上で、サーバーサイド(データベースとやり取りを行う)とフロントエンド（ブラウザ）でデータのやり取りを行う必要があります。そこでブラウザからサーバー側へHTTPリクエストを送る役割をAxiosが担ってくれます。

##準備
必要なものをインストールします。

````
npm install -g  @vue-cli
````
プロジェクトを起動します。

````
vue create frontend
````
フォルダを移動しサーバーを起動してみます。

````
cd frontend

npm run serve
````
こんな感じの画面が出ればOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1375933/fc7fcea4-0748-8af4-9959-70199ee5bfb4.png)

次にAxiosとVuexインストールします。

````
npm install axios --save

npm install vuex --save

npm install vuex-persistedstate --save
````

最後のはローカルストレージに自動でTokenを保存してくれるプラグインです。srcディレクトリ直下にhelperフォルダとstoreフォルダを作成します。
##コード入力
実際にセットアップしていきます。
storeフォルダ内にindex.jsファイルを作成し、以下のコードを入力します。

````js:store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import account from './modules/account'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)
const store = new Vuex.Store({
    plugins: [createPersistedState()],
    modules: {
        account
    }
})
````
次に同じくstoreディレクトリ内にmodulesフォルダを作成します。その中にaccount.jsファイルを作成し以下のコードを入力します。

````js:store/modules/account.js
import axios from "axios"

const state = {
    accessToken: '',
    refreshToken: '',
    loggedInUser: {},
    isAuthenticated: false,
}

const mutations = {
    setAccessToken: function(state, accessToken) {
        state.accessToken = accessToken;
    },//accessTokenをセットする
    setRefreshToken: function(state, refreshToken) {
        state.refreshToken = refreshToken;
    },//refreshTokenをセットする
    setLoggedInUser: function(state, payload) {
        state.loggedInUser = payload;
        state.isAuthenticated = true;
    },//ログイン成功時にuser情報をセットする
    clearUserData: function(state) {
        state.accessToken = '';
        state.refreshToken = '';
        state.loggedInUser = {};
        state.isAuthenticated = false;
    }//ログアウト時にuser情報をlocalからすべて破棄する
}

const getters = {
    accessToken: (state) => state.accessToken, // localからaccessTokenを取得
    refreshToken: (state) => state.refreshToken, // localからrefreshTokenを取得
    isAuthenticated: (state) => state.isAuthenticated, // userがログインしているか確認
    loggedInUser: (state) => state.loggedInUser, // user情報を取得
}

const actions = {
    logIn: ({ commit, dispatch }, credentials) => {
        return new Promise((resolve, reject) => {
            const loginURL = '/api/token/'
            const data = {
                email: credentials.email,
                password: credentials.password
            }//email, passwordを渡してtokenを取得する
            axios.post(loginURL, data)
                .then(res => {
                    commit('setAccessToken', res.data.access);
                    commit('setRefreshToken', res.data.refresh);
                    dispatch('fetchUser', res.data.id);
                    resolve(res)
                })//accesstoken, refreshtokenと同時にuserのidを取得しfetchUser関数に渡す
                .catch(err => {
                    console.log(err);
                    reject(err)
                })
        })
    },
    logOut: ({ commit, state }) => {
        return new Promise((resolve, reject) => {
            const logoutURL = '/api/token/logout/'
            const data = {
                refresh: state.refreshToken
            }
            axios.post(logoutURL, data) 
                .then(res => {
                    commit('clearUserData');
                    resolve(res);
                })//refreshtokenを渡しログアウトする
                .catch(err => {
                    commit('clearUserData');
                    console.log(err.config)
                    reject(err);
                })
        })
    }, 
    register: ({ dispatch }, payload) => {
        return new Promise((resolve, reject) => {
            const registerURL = '/api/users/'
            const user = {
                email: payload.email,
                password: payload.password,
                username: payload.username
            } // user情報はid email password usernameの4つ
            axios.post(registerURL, user)
                .then(res => {
                    dispatch('logIn', {
                        email: user.email,
                        password: user.password
                    })
                    resolve(res);
                })
                .catch(err => {
                    console.log(err);
                    reject(err);
                })
        })
    },
    refreshToken: ({ commit, state }) => {
        return new Promise((resolve, reject) => {
            const refreshURL = '/api/token/refresh/'
            const data = {
                refresh: state.refreshToken
            } // accessTokenの期限が切れている時にこの関数を呼び出す
            axios.post(refreshURL, data)
                .then(res => {
                    commit('setAccessToken', res.data.access);
                    commit('setRefreshToken', res.data.refresh);
                    resolve(res);
                }) // refreshTokenをサーバー側に渡し新しいtokenを取得
                .catch(err => {
                    console.log(err);
                    reject(err);
                }) // loginしているuserでないときerrorをcatchする
        })
    },
    fetchUser: ({ commit }, user_id) => {
        return new Promise((resolve, reject) => {
            const userURL = `/api/users/${user_id}/` // userの詳細データをAPIから取得
            axios.get(userURL)
                .then(res => {
                    commit('setLoggedInUser', res.data);
                    resolve(res);
                })
                .catch(err => {
                    console.log(err);
                    reject(err);
                })
        })
    }
}

export default {
    state,
    getters,
    mutations,
    actions
}
````

次にアクセストークンが切れている際に自動でリフレッシュトークンをサーバーにpostして新しいトークンを取得するようにプログラムします。先ほどのhelperディレクトリ内にaxios.jsファイルを作成し以下のコード入力します。

````js:helper/axios.js
import axios from 'axios'
import store from '../store/index'
import router from '../router'

export default function axiosSetUp() {
    axios.defaults.baseURL = ''; // 自分のAPIのベースURL e.g. http://localhost:8080
    axios.interceptors.request.use(
        function(config) {
            const token = store.getters.accessToken;
            if (token) {
                config.headers.Authorization = `Bearer ${token}`; // AuthヘッダーにaccessTokenを添える
            }
            return config;
        },
        function(error) {
            return Promise.reject(error);
        }
    )
}

axios.interceptors.response.use(
    function(response) {
        return response; //errorをcatchしない場合はそのままresponseを渡す
    },
    async function(error) {
        const originalRequest = error.config;
        if (
            error.response.status == 401 &&
            originalRequest.url.includes('/api/token/refresh/') // 401はユーザーが認証されていないことを示す
        ) {
            store.commit('clearUserData');
            router.push({ name: 'login' });
            return Promise.reject(error);
        } else if (error.response.status == 401 && !originalRequest._retry) {
            originalRequest._retry = true; 
            store.dispatch('refreshToken')
                .then(res => {
                    if (originalRequest.url.includes('/api/token/logout/')) {
                        originalRequest.data = JSON.stringify({ refresh: res.data.refresh });
                        axios(originalRequest); // logout時にはrefreshTokenを使うのでこの場合だけ分ける
                    } else axios(originalRequest); // tokenが無事取得出来たら再度ユーザーのリクエストを行う
                })
        }
        return Promise.reject(error);
    }
)
```

最後にaxiosのセットアップを行えば終了です。srcディレクトリ直下のmain.jsファイルに以下を入力します。


````js:main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store/index'
import axiosSetUp from './helper/axios'

Vue.config.productionTip = false

axiosSetUp()

new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')

````
##終わりに

いつかバックエンドの記事も上げてみます。
お疲れさまでした。