Nuxt.js で axios のヘッダに認証用トークンを付与したりトークンを更新したりを共通化する

# やりたいこと

- Nuxt.js からリソースサーバーにリクエストするときにヘッダーにトークンを付与したい。
- トークンの有効期限が切れていたら更新したい。

# 課題

リソースサーバーへリクエストするコンポーネント or ストアから axios にヘッダーを設定したり、コールバックをハンドリングするのはなかなか大変なので一箇所にまとめたい。

# 解決方法

## interceptors

axios には [interceptors](https://github.com/axios/axios#interceptors) という仕組みがあり、リクエスト、レスポンスをハンドリングする前に処理を割り込みさせることが可能です。これを利用することで共通化することができます。

>You can intercept requests or responses before they are handled by then or catch.

## 全てのリクエストにヘッダーの付与する

```typescript
  axios.interceptors.request.use((config: AxiosRequestConfig) => {
    config.headers = { token: token } // todo: ここでトークンを付与する
    return config
  })
```


## トークンの有効期限が切れていたら更新する

```typescript
  axios.interceptors.response.use((response => response), (error => {
    if (error.response.status === 401) {
      // todo: トークンの更新処理 
      return;
    }
  }))
```

## API クライアントモジュールを作成

上記の interceptors の設定は plugins/ 配下に api.ts のようなファイルを作成しました。これを `nuxt.config.js` の `plugins` プロパティに追記することでアプリ起動時にロードされます。

```nuxt.config.js
export default {
  plugins: [
    { src: '~/plugins/api.ts' },
  ]
}
```

# 実際のコード

**リクエストに共通のヘッダーを付与する**

実際のユースケースを考慮すると、多くの場合が Cookie や Vuex に保存済みのトークンをリクエストに付与するのではないでしょうか。その場合、 plugins プロパティは Nuxt のコンテキストを引数に取ることができます。つまり `store` も取得可能です。

```~/plugins/api.ts

export default function setup({ store }) {
  api.interceptors.request.use((config: AxiosRequestConfig) => {
    config.headers = { token: store.state.user.token }
    return config
  })
}
```

それ以外 plugins で受け取ることができるパラメータは公式の[こちらのページ](https://ja.nuxtjs.org/api/context/#%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88)にあります。

**アクセストークンを更新する**

アクセストークンの有効期限が切れたら、トークンを更新する必要があります。
トークン更新の方法は[公式の Issue にあったもの](https://github.com/axios/axios/issues/934#issuecomment-322003342)がわかりやすかったので、これをベースに少し修正を加えます。

```~/plugins/api.ts
let id

export default function setup({ store }) {
  if (!!id || id === 0) {
    return
  }

  id = axios.interceptors.response.use(null, (error) => {
    if (error.config && error.response && error.response.status === 401) {
      return updateToken().then((newToken) => {
        store.dispatch('user/userToken', newToken) // トークンを保存
        error.config.headers.xxxx // ヘッダーに新しいトークンを追加
        return axios.request(error.config);
      });
    }
    return Promise.reject(error);
  });
}
```

**ポイント**：認証エラー時にはアクセストークンを更新してからリトライをしたいので、 error が発生したリクエストの情報が必要になります。その情報は error.config にあるのでこれを axios.request() のパラメータに渡してあげればリトライが可能です。

**リダイレクトする**

アクセストークンの更新に失敗した場合やトークンを更新する必要がない場合、ユーザー認証画面へ飛ばしたいというケースがあると思います。

そんな時は plugins 配下のファイルであれば Nuxt の Context を受け取ることができるので `redirect` を利用することが可能です。

```~/plugins/api.ts
let id

export default function setup({ store, redirect }) {
  if (!!id || id === 0) {
    return
  }

  id = axios.interceptors.response.use(null, (error) => {
    if (error.config && error.response && error.response.status === 401) {
      redirect('/')
    }
    return Promise.reject(error);
  });
}
```

追記
----
- axios.interceptors.response.use を追加するかどうかの判定処理を追加しました。
- メモリリークの原因となります。詳細は [こちら](https://techblog.timers-inc.com/entry/2020/03/06/183134)

参考
----

この記事を書くにあたり参考にしたものです。

https://ja.nuxtjs.org/guide/plugins/
https://ja.nuxtjs.org/api/configuration-plugins/
https://github.com/axios/axios#interceptors
https://github.com/axios/axios/issues/934#issuecomment-322003342