在現代 Vue.js 應用程序中,Pinia 和 Vue Router 的結合使用為我們提供了強大的狀態管理和路由控制能力。今天,我們將深入探討如何將這兩個工具整合,特別是如何利用 Pinia plugins 來增強路由功能,並實現基於複雜應用狀態的導航守衛。我們還會特別關注如何將這些概念與 Day10 RBAC
和 ABAC
的概念結合,實現更加精細和靈活的權限控制。本文比較偏向把先前的概念整合,並和在一起,擔心讀者消化不良,但每個主題卻深入展示,今天就以一個簡短的整合,因此程式碼上會較先前有些簡化。
首先,讓我們創建一個 Pinia plugin,將 router 注入到每個 store 中:
import { PiniaPluginContext } from 'pinia'
import { Router } from 'vue-router'
export function routerPlugin(router: Router) {
return ({ store }: PiniaPluginContext) => {
store.router = router
}
}
在主應用文件中使用這個 plugin:提醒
:這個方法有在 Day11 的末尾有提到過,那裡有更進階的應用,這裡是簡單的展示。
import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
import { routerPlugin } from './plugins/routerPlugin'
const router = createRouter({
history: createWebHistory(),
routes: [
// 你的路由配置
]
})
const pinia = createPinia()
pinia.use(routerPlugin(router))
// 在你的 app 創建中使用 pinia 和 router
首先,我們定義一個權限 store:
import { computed } from 'vue';
import { PermissionRole } from "../achemas/auth";
import { usePermissionApi } from "../composables/usePermissionApi";
import { definePrivateState } from "./usePrivateState";
export const usePermissionStore = definePrivateState('usePermissionStore', () => {
return {
userRole: PermissionRole.None,
permissions: [] as string[]
}
}, (privateState) => {
const { getFakeUserPermissions } = usePermissionApi();
const fetchUserPermissions = async (): Promise<void> => {
const response = await getFakeUserPermissions()
privateState.userRole = response.role;
privateState.permissions = response.permissions;
};
const hasPermission = (permission: string): boolean => {
return privateState.permissions.includes(permission)
};
return {
userRole: computed(() => privateState.userRole),
permissions: computed(() => privateState.permissions),
fetchUserPermissions,
hasPermission,
}
})
備註:關於 definePrivateState
的使用可以參考 Day11
抓取 api 的部分可以參考
import { PermissionRole, permissionRoleSchema, PermissionRoleSchema } from '../achemas/auth';
export const usePermissionApi = () => {
const getFakeUserPermissions = async (): Promise<PermissionRoleSchema> => new Promise((resolve, reject) => {
setTimeout(() => {
const result: PermissionRoleSchema = {
role: PermissionRole.User,
permissions: []
};
const validator = permissionRoleSchema.safeParse(result);
if (!validator.success) {
reject(new TypeError('zod type error'));
return;
}
resolve(validator.data);
}, 200);
});
return {
getFakeUserPermissions,
};
};
export type UsePermissionApi = typeof usePermissionApi;
型別的部分:
import * as zod from 'zod';
export enum PermissionRole {
None = '',
Admin = 'admin',
Manager = 'manager',
User = 'user',
}
export const permissionRoleSchema = zod.object({
role: zod.nativeEnum(PermissionRole).refine(val => val !== PermissionRole.None),
permissions: zod.string().array(),
});
export type PermissionRoleSchema = zod.infer<typeof permissionRoleSchema>;
然後,我們可以實現基於角色的動態路由生成:
import { usePermissionStore } from '@/stores/usePermissionStore'
const permissionStore = usePermissionStore();
const { fetchUserPermissions, hasPermission } = permissionStore;
const { userRole, permissions } = storeToRefs(permissionStore);
await fetchUserPermissions();
const roleRoutes: Record<Exclude<PermissionRole, PermissionRole.None>, RouteRecordRaw[]> = {
[PermissionRole.Admin]: [
{ path: '/admin', component: () => import('../pages/Admin.vue') },
{ path: '/users', component: () => import('../pages/Users.vue') }
],
[PermissionRole.Manager]: [
{ path: '/dashboard', component: () => import('../pages/ManagerDashboard.vue') },
{ path: '/reports', component: () => import('../pages/Report.vue') }
],
[PermissionRole.User]: [
{ path: '/profile', component: () => import('../pages/UserProfile.vue') },
{ path: '/orders', component: () => import('../pages/Order.vue') }
],
}
const currentRoleRoutes = userRole.value === PermissionRole.None ? [] : roleRoutes[userRole.value];
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('../pages/Dashboard.vue'),
children: [
{
path: '/',
name: RoutesStatus.Home,
component: () => import('../pages/Home.vue'),
},
],
},
{
path: '/:catchAll(.*)',
name: RoutesStatus.NotFound,
component: () => import('../pages/NotFound.vue')
},
...currentRoleRoutes // 新添加權限的路由
]
實現導航守衛:
const isString = (input: unknown): input is string => {
return typeof input === 'string';
};
router.beforeEach((to) => {
const permissionTag = to.meta.requiredPermission;
if (!permissionTag) return true;
if (!isString(permissionTag) || !hasPermission(permissionTag)) return { name: RoutesStatus.Home };
return true;
});
ABAC 允許我們基於多個屬性進行更
精細的權限控制。讓我們擴展我們的 PermissionStore 來支援 ABAC:
import { computed } from 'vue';
import { PolicyAttributesSchema, PolicySchema } from "../achemas/auth";
import { usePermissionApi } from "../composables/usePermissionApi";
import { definePrivateState } from "./usePrivateState";
export const usePermissionStore = definePrivateState('usePermissionStore', () => {
return {
userAttributes: [] as PolicyAttributesSchema[],
policies: [] as PolicySchema[]
}
}, (privateState) => {
const { getFakePolicies, getFakeAttributes } = usePermissionApi();
const fetchPolicies = async (): Promise<void> => {
const policies = await getFakePolicies();
privateState.policies = policies;
};
const fetchUserAttributes = async (): Promise<void> => {
const policies = await getFakePolicies();
privateState.policies = policies;
};
const evaluateAccess = (resource: string, action: string): boolean => {
return privateState.policies.some(policy => {
return policy.resource === resource &&
policy.action.some((item) => item === action) &&
policy.attributes.every(item => {
return privateState.userAttributes.some(attr => {
return item === attr;
})
})
})
};
return {
userAttributes: computed(() => privateState.userAttributes),
policies: computed(() => privateState.policies),
fetchPolicies,
fetchUserAttributes,
evaluateAccess
}
});
型別的部分
export enum PolicyAction {
Create = 'create',
Edit = 'edit',
Delete = 'delete',
Query = 'query',
}
export const policyActionSchema = zod.nativeEnum(PolicyAction);
export type PolicyActionSchema = zod.infer<typeof policyActionSchema>;
export const policyAttributesSchema = zod.string();
export type PolicyAttributesSchema = zod.infer<typeof policyAttributesSchema>;
export const policySchema = zod.object({
resource: zod.string(),
action: zod.nativeEnum(PolicyAction).array(),
attributes: policyAttributesSchema.array(),
});
export type PolicySchema = zod.infer<typeof policySchema>;
製作假的 api 測試
import { PermissionRole, permissionRoleSchema, PermissionRoleSchema, PolicyAction, policyActionSchema, PolicyActionSchema, policyAttributesSchema, PolicyAttributesSchema, policySchema, PolicySchema } from '../achemas/auth';
export const usePermissionApi = () => {
const getFakePolicies = async (): Promise<PolicySchema[]> => new Promise((resolve, reject) => {
setTimeout(() => {
const result: PolicySchema[] = [{
resource: 'testSample',
action: [PolicyAction.Query, PolicyAction.Create],
attributes: ['hello'],
}];
const validator = policySchema.array().safeParse(result);
if (!validator.success) {
reject(new TypeError('zod type error'));
return;
}
resolve(validator.data);
}, 200);
});
const getFakeAttributes = async (): Promise<PolicyAttributesSchema[]> => new Promise((resolve, reject) => {
setTimeout(() => {
const result: PolicyAttributesSchema[] = ['hello'];
const validator = policyAttributesSchema.array().safeParse(result);
if (!validator.success) {
reject(new TypeError('zod type error'));
return;
}
resolve(validator.data);
}, 200);
});
return {
getFakePolicies,
getFakeAttributes,
};
};
export type UsePermissionApi = typeof usePermissionApi;
現在,我們可以在路由守衛中使用這個增強版的權限控制:
router.beforeEach(async (to, from, next) => {
const permissionStore = usePermissionStore()
if (!permissionStore.userAttributes.length) {
await permissionStore.fetchUserAttributes()
}
if (!permissionStore.policies.length) {
await permissionStore.fetchPolicies()
}
if (!to.meta.requiredAccess) return true;
const { resource, action } = to.meta.requiredAccess
if (!permissionStore.evaluateAccess(resource, action)) {
return { name: RoutesStatus.Home };
}
return true;
})
通過結合 Pinia 和 Vue Router,並整合 RBAC 和 ABAC 的概念,我們可以實現一個極其靈活和強大的權限控制系統。這種方法允許我們基於用戶的角色、屬性以及細粒度的策略來控制路由訪問。
這種高級的整合不僅提高了應用的安全性,還提供了極大的靈活性,使我們能夠根據不同的場景和需求輕鬆調整訪問控制邏輯。通過使用 Pinia store 來管理權限狀態,我們還能在整個應用中方便地訪問和使用這些權限信息,不僅用於路由控制,還可用於 UI 渲染和其他業務邏輯。
在實際應用中,記得要根據你的具體需求和安全要求來調整這些策略。同時,也要考慮性能影響,可能需要實現緩存機制來優化頻繁的權限檢查。