無感刷新とは何か、およびその実装方法

無感刷新とは、トークンが期限切れになる前に自動的に新しいトークンをリクエストし、ユーザーの介入なしにシームレスな操作を実現するメカニズムです。この方法により、ユーザー体験が向上し、トークンの期限切れによるログイン中断を回避できます。

無感刷新を実現する一般的な方法

トークンの有効期限管理

  • ユーザーがログインすると、フロントエンドは通常、アクセストークン(JWTなど)とリフレッシュトークンを保存します。
  • アクセストークンは通常、比較的短い有効期限(例えば15分)を持ち、リフレッシュトークンは長い有効期限(例えば7日間)を持ちます。
  • フロントエンドは、アクセストークンの有効期限が近づくと(例えば残り2分の場合)、リフレッシュトークンを使用してバックエンドに新しいアクセストークンをリクエストします。

インターセプター(Interceptor)

  • フロントエンドアプリケーションは、インターセプター(例えばAxiosやFetchの中で)を使用してすべてのリクエストをインターセプトできます。
  • 現在のアクセストークンが期限切れの場合、インターセプターはリフレッシュトークンを使用して新しいアクセストークンを取得し、元のリクエストを再送信します。
  • これにより、ユーザーは中断を感じることなく、アプリケーションは連続して操作できます。

無感刷新のロジック

  • トークンが期限切れになる前に自動的にリフレッシュ操作をトリガーするタイマーを設定します。
  • 定期的にトークンの有効期限をチェックし、トークンが期限切れになりそうな場合は自動的にリフレッシュして、ユーザー操作が中断されないようにします。
  • リフレッシュトークンも期限切れの場合は、ユーザーをログインページにリダイレクトします。

サイレント認証(Silent Authentication)

  • 一部のフロントエンドアプリケーションでは、隠しiFrameを使用してサイレント認証を行い、SSOメカニズムを利用してバックエンドでトークンのリフレッシュを完了させ、フロントエンドのユーザー操作には影響を与えません。

Axios インターセプター実装例

以下は、JWTトークンの自動リフレッシュとリクエスト再試行を処理するための完全なAxiosインターセプター実装コードです。この例では、リフレッシュトークン機能を提供するバックエンドAPIがあると仮定しています。

1. Axiosインスタンスの作成

まず、Axiosインスタンスを作成し、リクエストインターセプターとレスポンスインターセプターを設定します。

1
import axios from "axios";
2
3
// Axiosインスタンスの作成
4
const apiClient = axios.create({
5
baseURL: "https://api.example.com", // あなたのAPI基本URLに置き換えてください
6
timeout: 10000,
7
});

2. リクエストインターセプターの追加

各リクエストにAuthorizationヘッダーを追加して、JWTアクセストークンをリクエストに含めます。

1
// リクエストインターセプターの追加
2
apiClient.interceptors.request.use(
3
(config) => {
4
// 各リクエストにAuthorizationヘッダーを追加
5
const token = localStorage.getItem("token");
6
if (token) {
7
config.headers["Authorization"] = `Bearer ${token}`;
8
}
9
return config;
10
},
11
(error) => {
12
// リクエストエラーの処理
13
return Promise.reject(error);
14
},
15
);

3. レスポンスインターセプターの追加

401エラーを処理し、トークンが期限切れの場合にリフレッシュトークンを使用して新しいアクセストークンを取得します。

1
// レスポンスインターセプターの追加
2
apiClient.interceptors.response.use(
3
(response) => {
4
// レスポンスデータをそのまま返す
5
return response;
6
},
7
async (error) => {
8
const originalRequest = error.config;
9
10
// レスポンスステータスコードが401で、元のリクエストが再試行されていない場合
11
if (
12
error.response &&
13
error.response.status === 401 &&
14
!originalRequest._retry
15
) {
16
originalRequest._retry = true;
17
try {
18
const refreshToken = localStorage.getItem("refreshToken");
19
if (refreshToken) {
20
// リフレッシュトークンリクエストを送信
21
const { data } = await axios.post(
22
"https://api.example.com/auth/refresh",
23
{ token: refreshToken },
24
);
25
26
// ローカルストレージのトークンを更新
27
localStorage.setItem("token", data.token);
28
29
// Authorizationヘッダーを更新
30
axios.defaults.headers.common["Authorization"] =
31
`Bearer ${data.token}`;
32
33
// 元のリクエストを再送信
34
originalRequest.headers["Authorization"] = `Bearer ${data.token}`;
35
return apiClient(originalRequest);
36
}
37
} catch (refreshError) {
38
// リフレッシュトークンも無効な場合、ログインページにリダイレクト
39
localStorage.removeItem("token");
40
localStorage.removeItem("refreshToken");
41
window.location.href = "/login";
42
}
43
}
44
45
// その他のエラーを処理
46
return Promise.reject(error);
47
},
48
);
49
50
export default apiClient;

コードの説明

  1. Axiosインスタンスの作成:

    axios.create()を使用してカスタムAxiosインスタンスapiClientを作成し、基本URLやその他の設定を指定します。

  2. リクエストインターセプター:

    各リクエストが送信される前に、JWTアクセストークンが存在するかをチェックし、存在する場合はリクエストヘッダーAuthorizationに追加します。

  3. レスポンスインターセプター:

    • 401エラーの処理:サーバーが401未認証エラーを返すと、トークンが期限切れである可能性があります。
    • トークンのリフレッシュ:リフレッシュトークンが存在する場合、リフレッシュトークンのAPIエンドポイントにリクエストを送信して新しいアクセストークンを取得します。
    • トークンの更新:新しいトークンをローカルストレージに保存し、Axiosインスタンスのデフォルトリクエストヘッダーを更新して、今後のリクエストで新しいトークンを使用します。
    • 元のリクエストの再試行:新しいトークンを使用して元のリクエストを再送信します。
  4. リフレッシュトークンの失敗:

    リフレッシュトークンが無効または期限切れの場合、ローカルストレージのトークンをクリアし、ユーザーをログインページにリダイレクトします。

使用方法

アプリケーション内で、apiClientを使用してHTTPリクエストを送信できます:

1
import apiClient from "./apiClient";
2
3
async function getUserData() {
4
try {
5
const response = await apiClient.get("/user/profile");
6
console.log(response.data);
7
} catch (error) {
8
console.error("ユーザーデータの取得に失敗しました:", error);
9
}
10
}

このコードは、ラップされたapiClientを通じてリクエストを送信し、トークンのリフレッシュと再試行ロジックを自動的に処理します。

まとめ

以上の方法を使用することで、フロントエンドアプリケーションで無感刷新を実現し、トークンの期限切れによる中断を防ぐことができます。このメカニズムにより、ユーザー体験が向上し、アプリケーションがよりスムーズに動作します。