📱

【Capacitor】タスキルしてもログイン状態を維持したい!

2024/11/22に公開

はじめに

Web ベースのコードを Capacitor でモバイルアプリに移行する際、ログイン状態を適切に管理する方法をご紹介します。

Capacitor は Web アプリをモバイルアプリとして動作させるためのライブラリで、これにより Web 技術を使用してクロスプラットフォームなアプリを構築できます。私が所属しているサービスでは、Web 環境において Cookie を使用してログイン状態を維持していました。しかしモバイル環境では、タスクキルすると次回起動時にログイン状態がリセットされる問題が生じました。

この問題を解決するため、@capacitor/preferences を使用して、モバイルアプリ上におけるアクセストークンを永続化することにしました。
https://capacitorjs.jp/docs/apis/preferences

注意点

https://zenn.dev/wadeen/articles/b7f9feb2c39bc1

認証方法

トークン認証(Bearer Token)を用いてアクセストークンを Cookie に保存しています。そのためセッション認証ではないことに注意してください。周辺知識については以下の記事で解説されてました。
https://zenn.dev/oreilly_ota/articles/31d66fab5c184e
https://zenn.dev/tell1124/articles/c914cdd2e30bee

動作環境

ツール バージョン 用途
Nuxt 3.11.2 フレームワーク
Vue 3.4.27 UI ライブラリ
Pinia 2.1.7 状態管理
TypeScript 5.5.2 型定義
@capacitor/core 6.0.0 Capacitor のコアパッケージ
@capacitor/preferences 6.0.1 モバイルの永続ストレージ用プラグイン
js-cookie 3.0.5 Cookie 操作のためのユーティリティ
jose 5.4.0 JWT トークンのエンコード/デコード
moment-timezone 0.5.45 日時とタイムゾーンの操作

実装手順

今回紹介する実装のディレクトリ構成は以下の通りです。主な import はimports.d.tsでまとめているので省略します。

frontend/
├── composables/
│   ├── storages/
│   │   └──auth/
│   │       ├── appAuthStorage.ts         # モバイル用認証ストレージ (@capacitor/preferences)
│   │       ├── authStorage.ts            # Web とモバイル環境の切り分けロジック
│   │       └── webAuthStorage.ts         # Web 用認証ストレージ (Cookie ベース)
│   └── resolvers/
│          └── nativeAppResolver.ts      # 環境判定 (Web かモバイルか)
├── stores/
│   └── auth.ts                       # Pinia ストアによる認証情報の管理
└── interfaces/
    └── stores/
        └── authInterface.ts          # 認証ストアのインターフェース(実装は省略)

1. Setup

まず、Capacitor の@capacitor/preferences プラグインをインストールします。

npm install @capacitor/preferences

もしくは yarn を使用する場合は次のコマンドです。

yarn add @capacitor/preferences

2. Web とモバイルでの認証状態管理の切り分け

Web 環境では Cookie を利用してアクセストークンを管理し、モバイル環境では @capacitor/preferences を利用するように切り分けます。
以下の表はトークン管理における各メソッドの役割をまとめたものです。

メソッド Web
(js-cookie)
App
(@capacitor/preferences)
用途
get() Cookies.get(key) Preferences.get({ key }) 認証情報の確認
remove() Cookies.remove(key) Preferences.remove({ key }) ログアウトやトークンの期限切れなどで使用
set() Cookies.set(key, token, options) Preferences.set({ key, value }) 認証情報の永続化

Web 用認証ストレージ (webAuthStorage)

以下のコードでは、Web 用の認証として、js-cookie を使用して アクセストークンを Cookie で管理するためのメソッドを定義しています。

webAuthStorage.ts
import Cookies from "js-cookie";

export const useWebAuthStorage = () => {
  const key = 'key'; //任意のキー

  const get = () => Cookies.get(key);
  const remove = () => Cookies.remove(key);
  const set = (token: string, options: object) => Cookies.set(key, token, options);

  return {
    get,
    remove,
    set,
  };
};

モバイルアプリ用認証ストレージ (appAuthStorage)

モバイルアプリでは、@capacitor/preferences を使用してデータを永続化します。

appAuthStorage.ts
import { Preferences } from "@capacitor/preferences";

export const useAppAuthStorage = () => {
  const key = 'key'; //任意のキー

  const get = async () => {
    const { value } = await Preferences.get({ key });
    return value;
  };
  const remove = () => Preferences.remove({ key });
  const set = (value: string) => Preferences.set({ key, value });

  return {
    get,
    remove,
    set,
  };
};

3. Web とモバイルでの認証ストレージの分岐

Web 環境とモバイル環境の認証ストレージを条件分岐させます。nativeAppResolver.ts を使ってプラットフォームを判定し、モバイルアプリの場合は appAuthStorage.ts、それ以外は webAuthStorage.ts を使用します。

authStorage.ts
export const useAuthStorage = () => {
  const nativeAppResolver = useNativeAppResolver();
  return nativeAppResolver.isNative.value ? useAppAuthStorage() : useWebAuthStorage();
};

nativeAppResolver.ts でプラットフォームを判定し、ネイティブアプリかどうかを確認します。

nativeAppResolver.ts
import { Capacitor } from "@capacitor/core";

export const useNativeAppResolver = () => {
  const isNative = computed<boolean>(() => Capacitor.isNativePlatform());
  return {
    isNative,
  };
};

4. 認証状態の管理

Pinia を利用して、認証情報の管理をします。認証ストアは、トークンの有効期限や状態の管理を担い、Web 環境とモバイル環境の違いに応じてauthStorage.tsで認証ストレージを切り替えています。
この例では、JWT トークンのデコード結果からトークンの有効期限を取得し、その有効期限の 10 分前にクッキーを失効させることで、トークンの有効期限が切れる前にリフレッシュ処理を行うことができます。また、セキュリティ対策として sameSite を指定することで、CSRF 対策しています。
(httpOnly などはサーバ側で付与します。)
https://qiita.com/kotacoffee/items/febd893aa846d739880b

auth.ts
import { defineStore } from "~/interfaces/stores/authInterface"; //型定義用
import * as jose from "jose";
import moment from "moment-timezone";

export const useAuthStore = defineStore({
  id: "auth",
  state: () => ({
    token: "",
    tokenExp: "",
  }),
  getters: {
    isLogin: (state) => !!state.token && moment() < moment(state.tokenExp ?? 0),
  },
  actions: {
    reset() {
      this.$reset();
      useAuthStorage().remove();
    },
    store(token) {
      const decodedToken = jose.decodeJwt(token ?? "");
      if (decodedToken) {
        this.token = token;
        this.tokenExp = (decodedToken?.exp ?? 0) * 1000;
        useAuthStorage().set(token, {
          path: "/",
          sameSite: "strict",
          expires: moment(this.tokenExp).subtract(10, "minutes").toDate(),
        });
      } else {
        this.reset();
      }
    },
  },
});

最終的には Middleware において、isLoginfalse 場合に、authStorage.tsから取得したtokenを store メソッドで再保存するのが良いと思います。

Capacitor に対する印象(余談)

個人的には、Capacitor を使うことで Web とモバイルアプリを同時に管理できるため、人員が少なく、小中規模のプロダクトなら Capacitor という選択肢をとっても良いのかなといった印象でした。

ただしサービス規模が大きくなるに連れて、Web とモバイルアプリで別の UI や処理を実装することも多いので、フロントエンド側のファイルが増えつづけてしまい、管理が複雑になるという欠点もあるのかなと思っています。特にモバイル特有のネイティブな機能を多く取り入れる場合には、Capacitor を使うことがかえって負担になる気がしています。

さらに Capacitor に関する日本語のテックブログがまだそこまで多くなく、公式ドキュメントだけでは難しい場合もあるため、学習リソースの少なさがネックに感じる部分もあります。もう少し具体的な実装例が増えると、より多くの開発者が利用しやすくなるのではと思います。

まとめ

本記事では、Capacitor を使用して Web とモバイルで異なるログイン状態の管理を実現する方法を紹介しました。@capacitor/preferences プラグインにより、モバイルアプリでもアクセストークンを永続化できるため、タスクキル後でもログイン状態を維持できます。Web 環境とモバイル環境でストレージの管理方法を柔軟に切り分けることで、ユーザーの利便性を損なうことなく、認証情報の管理が可能になります。

ソーシャルデータバンク テックブログ

Discussion