🛡️

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

2023/12/31に公開

概要

どうもukmashiです。本記事では、1部目に引き続き、FEとBEで認可の処理が二元化してしまうのをどうクリアするかの提案する記事です。

1部目を読んでない方は、ぜひ読んでいただけると幸いです。

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

BEとFEの責務に関して

1部の最後でもお話ししましたが、BE、FEそれぞれの責務について再度まとめます。

  • BEはデータ操作に関する責任を持つ
  • FEは表示制御に関する責任を持つ

という形になります。

しかし、認可の処理は、BEとFEの両方で必要となります。

  • BE:管理者のみが実行可能なデータ操作
  • FE:管理者のみに描画させたいメニュー

1部では、責務分離の観点で、認可の情報の処理が二元化してしまうことを問題提起して終わりました。

ここで、一度、課題をまとめます。

  • 責務分離がされている
    • 密結合にならない(依存性がない)
    • 複雑性が増さない
  • 管理がしやすい
  • スケールしやすい

これらを解決するための解決案を本稿では提案していこうと思います。

複雑化する処理たち

課題を解決するにあたり、出てきた解決案を紹介します。

データ制御をBEに委託する

適切なユーザーに適切なメニューなどをJSONでBEから取得し、FEはJSONの内容を描画する案です。

責務分離の観点からして、FEは描画に専念し、BEはデータの操作に専念できている上で、認可の処理もBEのみに集約され、一見理想的です。

しかし、管理者だけに表示して欲しいメニューが、一般ユーザーにも表示されるなどがあった場合、本来、描画の制御を行うFEに対してデバッグを行うのに対し、この方法では、制御を行っているBEまでデバックの範囲を広げなくてはいけないので、実際には責務分離の観点からしてNGでした。

制御が二元化する。実際のアプリケーションでは、ユーザーの権限などでFormの項目を変化させたりする場合、認可の情報が必要です。結局、描画の制御をBEで行える部分はBEで行い、Formなどの制御はFEで行っており、制御が二元化してしまい、依存性も増して、複雑にしているだけですね。

二元管理を許容する

責務分離の観点からして、認可処理が二元化するのは仕方ないので許容するという案です。

BEとFEは疎結合になり、依存性が解消され、責務分離の観点でもメリットは大きいですが、何も解決していません。

簡単なアプリではこれでもいいですが、SaaS製品などではスケールしにくくなる欠点があります。

どう解決するか

本課題を解決するためにPBAC(Policy Based Access Control)を採用しました。

ポリシーについて

余談として、認可の種類について、少々触れておきます。

PBACとは、ユーザーにポリシーが付与されているかで、認可を行う方法です。別名、Attribute Based Access Controlともいいます。

RBAC(Role Based Access Control)は権限(管理者、ユーザー、ゲストなど)によって、認可を行うシンプルな方法です。ユーザーの料金プランなどに応じて、認可の条件を変えるなどを行いたい場合に、複雑性が増してしまったり、PBACと比べ、あまり柔軟性がないのが欠点です。

詳しくは下記を参考にしてください

https://www.okta.com/jp/blog/2020/09/attribute-based-access-control-abac/

解決方法

ログイン中のユーザーのポリシー情報をAPIから取得し、FEはユーザーが該当のポリシーを持っているかで描画制御を行う方法です。

これにより、ユーザーに対する認可の制御に関してはBEが責任を持ち、FEは描画対する制御に責任を持つことができ、責務分離の点を担保しています。

BEに認可の処理を寄せられるので、認可に関する仕様の変更があってもBEのみの修正で対応可能になるため、スケールしやすくなります。

下記はReactでのサンプルコードで解説します。

// APIから自分の情報を取得するHooks
const useUser = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 通常はここでAPI呼び出しを行い、ユーザーデータを取得します。
    // 今回はデモのために静的なデータをセットします。
    const userData = {
			id: '123',
			name: 'John Doe',
			email: 'john@example.com',
			policies: ['posts:list', 'posts:show', 'posts:edit', 'posts:delete']
		};

    setUser(userData);
  }, []);

  return user;
}

useUserはカスタムフックです。APIから自分の情報を取得してきます。

const Authorize = ({ policy, children, fallback = null }) => {
  const user = useUser();

  // ユーザー情報がまだ読み込まれていない場合は何も表示しないか、
  // オプションでローディング表示などを行う
  if (!user) {
    return <div>Loading...</div>;
  }

  // 指定されたポリシーをユーザーが持っているかどうかをチェック
  const isAuthorized = user.policies.includes(policy);

  // 認可されている場合は子コンポーネントを表示、そうでなければfallbackを表示
  return isAuthorized ? children : fallback;
};

Authorizeコンポーネントは、useUserを用いて、引数で指定されたポリシーをユーザーが持っているかを確認し、ポリシーを持っていれば内容を描画します。

const Menu = () => {
  return (
    <div>
      <h1>メインメニュー</h1>
      <ul>
        <li>ホーム</li>

        {/* posts:list ポリシーを持つユーザーにのみ表示 */}
        <Authorize policy="posts:list">
          <li>記事一覧</li>
        </Authorize>

        {/* posts:edit ポリシーを持つユーザーにのみ表示 */}
        <Authorize policy="posts:edit">
          <li>記事編集</li>
        </Authorize>

        {/* admin:access ポリシーを持つユーザーにのみ表示 */}
        <Authorize policy="admin:access">
          <li>管理者設定</li>
        </Authorize>
      </ul>
    </div>
  );
};

useUser、Authorizeを用いて、適切なユーザーに適切なメニューが描画されます。

本サンプルでは、Authorizeコンポーネントを使用するたびにAPIを叩きに行ってしまう欠点があるので、useCallbackを用いたり、ReactQueryでキャッシュするなど、読者の皆さんのプロダクトごとに最適化してください。
また、Post一覧で自分のPostに対してのみ編集ボタンを表示するなどの複雑な処理に関しても、まだ未解決です。

最後に

最後まで読んでいただきありがとうございます。
年末にずっと考えており、なんとか課題そのものを解決することがよかったですが、まだまだ機能としては未熟ですが、参考にしていただければ幸いです。
年明け、再度、思考をまとめて、続きを書ければと思います。

みなさま、良いお年をお迎えください

Discussion