🛡️

フロントエンドの認可について(その3)

2024/01/19に公開

概要

この記事でフロントエンドの認可について取り扱うのは3回目です。2回目の記事では、どうすれば良いかという提案で終わりましたが、2024年を迎え、フロントエンドの認可処理の実装を完了したので、今回はもっと具体的にコードに落とし込み、どのように実装したかを共有しようと思います。

1回目、2回目の記事をまだお読みでない方は、ぜひチェックしてみてください。

下記途中ではありますが、バックエンドの認可についても書いていますので、ぜひチェックしてみてください

https://zenn.dev/ukkyon/articles/5efad94f92ded3

この記事では、サンプルコードとしてReactを使用します。

認可の実装

ユーザー情報の取得

まず、ユーザー情報を取得するための関数を準備します。

今回の例ではTanStack Queryを使用しているので、ユーザー情報を取得するためのカスタムフックが以下になります。

export type Policy = {
  controller: string;
  action: string;
}

export type User = {
  firstName: string;
  firstNameKana: string;
  lastName: string;
  lastNameKana: string;
  role: string;
  plan: string;
  policies: Policy[];
} & BaseEntity;

const API_USER = '/api/v1/auth/user';

export const getUserApi = async (): Promise<AxiosResponse<User>> => {
  return await axios.get(API_USER)
}

export const useUser = () => {
  return useQuery(['user'], getUserApi);
};

このコードを使用すると、ログイン中のユーザーの権限、加入プラン、保有ポリシーを取得できます。

認可チェックHooks

次に、認可を行うためのHooksを定義します。

export const useAuthorization = () => {
  const { data: userData } = useUser();
  const response = userData.data;

  if (!userData) throw Error('User does not exist!');

  const checkAccess = React.useCallback(
    (policy: Policy, condition?: (user: User) => boolean) => {
      const userPolicies = response.user.policies;

      // userPoliciesに指定されたポリシーが存在するか確認
      const hasUserPolicy = userPolicies.some(
        (p) => p.controller === policy.controller && p.action === policy.action
      );
      if (!hasUserPolicy) return false;

      // カスタム条件関数の実行
      return condition ? condition(response.user) : true;
    },
    [response.user]
  );

  return { checkAccess, user: response.user };
};

このフックを使用して、指定されたポリシーに基づいてユーザーの認可を行います。

カスタムの条件関数を使用することで、より複雑な認可も可能になります。具体的な使用方法については後述します。

描画の制御

type AuthorizationProps = {
  forbiddenFallback?: React.ReactNode;
  children: React.ReactNode;
  policy: Policy; // Policyオブジェクトを追加
  condition?: (user: User) => boolean; // カスタム条件関数を追加
};

export const Authorization = ({
  policy,
  condition,
  forbiddenFallback = null,
  children,
}: AuthorizationProps) => {
  const { checkAccess } = useAuthorization();
  const canAccess = checkAccess(policy, condition);
  return <>{canAccess ? children : forbiddenFallback}</>;
};

Authorizationコンポーネントを使用することで、ユーザーが指定したポリシーを保有しているかどうかを確認し、持っていない場合は何も描画せず、またはforbiddenFallbackを指定して異なる内容を描画することで、ユーザー体験を向上させることができます。

使い方

処理制御

useAuthorizationフックを使用して、配列などの制御を行うことができます。

以下のサンプルでは、メニューを描画するためのポリシーを定義し、ロールに基づいて細かい認可を実施しています。これにより、ポリシーを持っているユーザーのみにメニューが描画されます。また、ポリシーを持っていても条件を満たさない場合は描画されません。

export type MenuItem = {
  name: string;
  url: string;
  policy: Policy;
  condition?: (user: User) => boolean;
};

const menuItems: MenuItem[] = [
  {
    title: 'Dashboard',
    url: '/dashboard',
    policy: { controller: 'dashboard', action: 'view' },
  },
  {
    title: 'Settings',
    url: '/settings',
    policy: { controller: 'settings', action: 'edit' },
    condition: (user) => user.role === 'admin',
  },
  // ... other menu items
];

const Menu = () => {
  const { checkAccess } = useAuthorization();

  const listItems = menuItems.map((menu, index) => {
    const canAccess = checkAccess(menu.policy, () => menu.condition ? menu.condition(user) : true);

    if (canAccess) {
      return (
        <li key={index}>
          <NavLink to={menu.url}>
            {menu.name}
          </NavLink>
        </li>
      );
    }
    return null;
  });

  return <ul>{listItems}</ul>;
};

描画制御

以下の例では、管理者以外が設定画面にアクセスした場合を考慮しています。

settings:editのポリシーを保有していても、管理者でなければforbiddenFallbackを描画することで、ユーザー体験を向上させます。

const SettingsPage = () => {
  const policy = { controller: 'settings', action: 'edit' };
  const condition = (user: User) => user.role === 'admin';

  return (
    <Authorization
      policy={policy}
      condition={condition}
      forbiddenFallback={<div>アクセス権がありません。</div>}
    >
      <div>
        設定ページの内容
      </div>
    </Authorization>
  );
};

export default SettingsPage;

最後に

これにて、「フロントエンドの認可ついて」シリーズは終了です。
全編に渡って読んでいただいた方々、ありがとうございます。
ぜひ、拡散していただけたら幸いです。

FE、BEともにマスターを保有しなくては行けない仕組みですが、認可の処理をバックエンドに寄せることで、FEへの修正は行わなくてもスケールしやすい認可処理を実装できたかと思います。

スケールしやすく、汎用性を持たせた認可処理の設計に結構大変でしたが、認可について色々考える機会であり、良かったです。

バックエンドの認可についても書いていこうと思うので、ぜひ読んでいただけたら幸いです。

https://zenn.dev/ukkyon/articles/5efad94f92ded3

Discussion