フロントエンドの認可について(その3)
概要
この記事でフロントエンドの認可について取り扱うのは3回目です。2回目の記事では、どうすれば良いかという提案で終わりましたが、2024年を迎え、フロントエンドの認可処理の実装を完了したので、今回はもっと具体的にコードに落とし込み、どのように実装したかを共有しようと思います。
1回目、2回目の記事をまだお読みでない方は、ぜひチェックしてみてください。
-
フロントエンドの認可について(1)
ReactやVueを始めとして、SPA、Next.js、Nuxt.jsに関する認可についてまとめます。 -
フロントエンドの認可について(2)
後半では、FEとBEで認可の処理が二元化してしまうのをどうクリアするかの提案です。 -
フロントエンドの認可について(3) ← 本記事
2での提案を具体的にReactのコードとして落とし込みました
下記途中ではありますが、バックエンドの認可についても書いていますので、ぜひチェックしてみてください
この記事では、サンプルコードとして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への修正は行わなくてもスケールしやすい認可処理を実装できたかと思います。
スケールしやすく、汎用性を持たせた認可処理の設計に結構大変でしたが、認可について色々考える機会であり、良かったです。
バックエンドの認可についても書いていこうと思うので、ぜひ読んでいただけたら幸いです。
Discussion