跳到主要内容

设计权限管理系统

问题

如何设计一个完善的前后端权限管理系统?从权限模型选型、前端三层权限控制、权限 SDK 设计到数据权限与缓存同步,请详细说明核心模块的设计思路与关键技术实现。

答案

权限管理系统是中后台应用的基础设施,需要解决"能在什么条件下对哪些资源执行什么操作"这一核心问题。一个完整的权限系统涉及权限模型设计、前端多层控制(路由/菜单/按钮)、后端鉴权、数据权限、权限 SDK、缓存同步等多个关键模块。前端权限控制是用户体验的保障,后端鉴权才是安全的底线。


一、需求分析

功能需求

模块功能点
用户管理用户增删改查、批量导入、状态启停
角色管理角色 CRUD、角色继承、角色分配
权限分配菜单权限、按钮权限、API 权限、数据权限
组织架构部门管理、岗位管理、用户-部门-角色关联
权限校验前端路由/菜单/按钮控制、后端接口鉴权
审计日志操作日志、权限变更记录、登录日志

非功能需求

指标目标
安全性前后端双重校验,防越权访问
性能权限校验 < 1ms,不阻塞正常业务请求
灵活性支持动态调整,权限变更实时生效
可扩展支持多租户、多系统、微前端权限隔离
易用性SDK 提供 React Hooks / Vue 指令等开箱即用的 API
核心原则

前端权限控制是体验优化(隐藏无权限的 UI),后端权限校验是安全保障(拦截无权限的请求)。两者缺一不可,但安全边界永远在后端。


二、权限模型演进

权限模型经历了从简单到复杂的演进过程,理解各模型的适用场景是系统设计的基础。

2.1 四种权限模型对比

模型全称核心思想典型场景
DACDiscretionary Access Control资源所有者自行决定谁能访问文件系统、Google Docs 共享
MACMandatory Access Control系统强制分配安全等级,不可自行变更军事系统、政府机密文件
RBACRole-Based Access Control通过角色间接关联用户和权限企业管理后台、SaaS 系统
ABACAttribute-Based Access Control基于属性(用户/资源/环境)动态决策云平台 IAM、复杂多租户

2.2 模型演进关系

2.3 各模型代码示例

models/permission-models.ts
// ========================
// 1. DAC - 自主访问控制
// ========================
interface DACResource {
ownerId: string;
acl: Map<string, Set<'read' | 'write' | 'delete'>>; // 访问控制列表
}

function checkDAC(userId: string, resource: DACResource, action: string): boolean {
// 所有者拥有全部权限
if (resource.ownerId === userId) return true;
// 检查 ACL
const userPerms = resource.acl.get(userId);
return userPerms?.has(action as 'read' | 'write' | 'delete') ?? false;
}

// ========================
// 2. RBAC - 基于角色的访问控制
// ========================
interface RBACUser {
id: string;
roles: string[];
}

interface RBACRole {
name: string;
permissions: string[];
parent?: string; // 角色继承
}

function checkRBAC(user: RBACUser, permission: string, roleMap: Map<string, RBACRole>): boolean {
for (const roleName of user.roles) {
let currentRole = roleMap.get(roleName);
// 沿继承链向上查找
while (currentRole) {
if (currentRole.permissions.includes(permission)) return true;
currentRole = currentRole.parent ? roleMap.get(currentRole.parent) : undefined;
}
}
return false;
}

// ========================
// 3. ABAC - 基于属性的访问控制
// ========================
interface ABACContext {
user: { id: string; department: string; level: number; roles: string[] };
resource: { type: string; ownerId: string; department: string; classification: string };
action: string;
environment: { time: Date; ip: string; deviceType: string }; // 环境因素
}

type ABACPolicy = (ctx: ABACContext) => boolean;

// ABAC 策略示例:只有同部门的经理才能在工作时间审批
const approvalPolicy: ABACPolicy = (ctx) => {
const hour = ctx.environment.time.getHours();
return (
ctx.user.department === ctx.resource.department &&
ctx.user.level >= 3 &&
hour >= 9 && hour <= 18
);
};
面试要点

RBAC 是最主流的选择,覆盖 90% 以上的后台管理系统。ABAC 通常作为 RBAC 的补充,用于需要基于属性(时间、地点、设备等)动态决策的场景。面试中重点掌握 RBAC 即可,能说出 ABAC 是加分项。


三、整体架构


四、RBAC 核心设计

4.1 数据模型

RBAC 的核心是 用户 - 角色 - 权限 三层模型,通过中间表实现多对多关联。

4.2 TypeScript 类型定义

types/permission.ts
/** 权限类型 */
type PermissionType = 'menu' | 'button' | 'api';

/** 权限节点 */
interface Permission {
id: string;
code: string; // 权限码,如 'user:create', 'order:list'
name: string; // 显示名称
type: PermissionType;
parentId: string | null;
path?: string; // 路由路径(菜单权限)
component?: string; // 组件路径(菜单权限)
icon?: string;
sort: number;
children?: Permission[]; // 树形结构
}

/** 角色 */
interface Role {
id: string;
code: string; // 如 'admin', 'editor', 'viewer'
name: string;
parentId: string | null; // 角色继承
permissions: string[]; // 权限码列表
dataScope: DataScope; // 数据权限范围
}

/** 数据权限范围 */
type DataScope = 'all' | 'department' | 'department_and_children' | 'self' | 'custom';

/** 用户权限信息(登录后获取) */
interface UserPermissionInfo {
userId: string;
username: string;
roles: string[]; // 角色码列表
permissions: string[]; // 扁平化权限码列表
menus: Permission[]; // 菜单树
dataScope: DataScope;
departmentId: string;
}

4.3 角色继承实现

角色继承允许子角色自动拥有父角色的所有权限,减少重复配置。

services/role-inheritance.ts
class RoleService {
private roleMap: Map<string, Role> = new Map();

/** 获取角色的完整权限(包含继承链) */
getEffectivePermissions(roleCode: string): Set<string> {
const permissions = new Set<string>();
const visited = new Set<string>(); // 防止循环继承

const collect = (code: string): void => {
if (visited.has(code)) return;
visited.add(code);

const role = this.roleMap.get(code);
if (!role) return;

// 添加当前角色权限
role.permissions.forEach((p) => permissions.add(p));

// 递归收集父角色权限
if (role.parentId) {
const parentRole = [...this.roleMap.values()].find((r) => r.id === role.parentId);
if (parentRole) collect(parentRole.code);
}
};

collect(roleCode);
return permissions;
}

/** 获取用户所有角色的合并权限 */
getUserPermissions(roles: string[]): string[] {
const allPerms = new Set<string>();
for (const role of roles) {
this.getEffectivePermissions(role).forEach((p) => allPerms.add(p));
}
return [...allPerms];
}
}
循环继承防护

角色继承必须做循环检测。在分配父角色时,应检查是否会形成闭环。上面的代码通过 visited Set 避免了无限递归,但更好的做法是在设置 parentId 时就进行校验。


五、前端权限控制三层

前端权限控制分为三个层次,从粗到细逐层过滤。

5.1 路由权限 - 动态路由

动态路由是前端权限的第一道防线。有两种主流方案。

方案对比

特性前端全量路由过滤后端返回路由表
实现复杂度
灵活性中(需前端发版)高(后端动态配置)
安全性低(前端包含全部路由信息)中(路由信息由后端控制)
维护成本前端维护前后端都需维护
适用场景中小项目、路由固定大型项目、路由频繁变更

方案一:前端全量路由过滤

router/filter-routes.ts
import type { RouteRecordRaw } from 'vue-router';

/** 所有路由(前端维护完整路由表) */
const asyncRoutes: RouteRecordRaw[] = [
{
path: '/user',
component: () => import('@/layouts/BasicLayout.vue'),
meta: { title: '用户管理', permission: 'user:list' },
children: [
{
path: 'list',
component: () => import('@/views/user/UserList.vue'),
meta: { title: '用户列表', permission: 'user:list' },
},
{
path: 'create',
component: () => import('@/views/user/UserCreate.vue'),
meta: { title: '创建用户', permission: 'user:create' },
},
],
},
{
path: '/system',
component: () => import('@/layouts/BasicLayout.vue'),
meta: { title: '系统管理', permission: 'system:manage' },
children: [
{
path: 'role',
component: () => import('@/views/system/RoleList.vue'),
meta: { title: '角色管理', permission: 'system:role' },
},
],
},
];

/** 根据用户权限过滤路由 */
function filterRoutes(routes: RouteRecordRaw[], permissions: string[]): RouteRecordRaw[] {
return routes.reduce<RouteRecordRaw[]>((acc, route) => {
const permission = (route.meta as { permission?: string })?.permission;

// 没有权限要求的路由直接通过
if (!permission || permissions.includes(permission)) {
const filteredRoute = { ...route };

// 递归过滤子路由
if (route.children) {
filteredRoute.children = filterRoutes(route.children, permissions);
}

// 只添加有子路由或叶子节点的路由
if (!route.children || filteredRoute.children!.length > 0) {
acc.push(filteredRoute);
}
}

return acc;
}, []);
}

方案二:后端返回路由表 + addRoute 动态注册

router/dynamic-routes.ts
import type { RouteRecordRaw } from 'vue-router';
import { router } from './index';

/** 后端返回的路由数据 */
interface ServerRoute {
path: string;
name: string;
component: string; // 组件路径字符串,如 'user/UserList'
meta?: Record<string, unknown>;
children?: ServerRoute[];
}

/** 组件映射表(Vite 的 glob import) */
const componentModules = import.meta.glob<{ default: unknown }>('/src/views/**/*.vue');

/** 将组件路径字符串解析为实际组件 */
function resolveComponent(componentPath: string): () => Promise<unknown> {
const fullPath = `/src/views/${componentPath}.vue`;
const module = componentModules[fullPath];
if (!module) {
console.warn(`组件不存在: ${fullPath}`);
return () => import('@/views/error/404.vue');
}
return module;
}

/** 将后端路由数据转换为 Vue Router 路由 */
function transformRoutes(serverRoutes: ServerRoute[]): RouteRecordRaw[] {
return serverRoutes.map((route) => ({
path: route.path,
name: route.name,
component: resolveComponent(route.component),
meta: route.meta ?? {},
children: route.children ? transformRoutes(route.children) : undefined,
}));
}

/** 动态注册路由 */
async function initDynamicRoutes(): Promise<void> {
const { data } = await fetch('/api/user/routes').then((res) => res.json());
const routes = transformRoutes(data as ServerRoute[]);

routes.forEach((route) => {
router.addRoute('Layout', route); // 添加到 Layout 路由下
});

// 添加 404 兜底路由(必须最后添加)
router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' });
}

React 动态路由方案

router/DynamicRoutes.tsx
import { Suspense, lazy, useMemo } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { usePermission } from '@/hooks/usePermission';

/** 组件懒加载映射 */
const componentMap: Record<string, React.LazyExoticComponent<React.ComponentType>> = {
'user/UserList': lazy(() => import('@/views/user/UserList')),
'user/UserCreate': lazy(() => import('@/views/user/UserCreate')),
'system/RoleList': lazy(() => import('@/views/system/RoleList')),
};

interface RouteConfig {
path: string;
component: string;
permission?: string;
children?: RouteConfig[];
}

function DynamicRoutes(): React.ReactElement {
const { userRoutes, hasPermission } = usePermission();

const renderRoutes = useMemo(() => {
const buildRoutes = (routes: RouteConfig[]): React.ReactNode[] =>
routes
.filter((route) => !route.permission || hasPermission(route.permission))
.map((route) => {
const Component = componentMap[route.component];
return (
<Route key={route.path} path={route.path} element={
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
}>
{route.children && buildRoutes(route.children)}
</Route>
);
});

return buildRoutes(userRoutes);
}, [userRoutes, hasPermission]);

return (
<Routes>
{renderRoutes}
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
);
}

export default DynamicRoutes;

5.2 菜单权限 - 侧边栏过滤

菜单权限基于路由过滤结果,渲染用户可见的侧边栏菜单。

utils/menu-filter.ts
interface MenuItem {
key: string;
title: string;
icon?: string;
path?: string;
children?: MenuItem[];
hidden?: boolean; // 隐藏菜单但保留路由(如详情页)
}

/** 根据权限过滤菜单(排除隐藏项和无权限项) */
function filterMenus(menus: MenuItem[], permissions: string[]): MenuItem[] {
return menus.reduce<MenuItem[]>((acc, menu) => {
// 隐藏的菜单不显示
if (menu.hidden) return acc;

const filteredMenu = { ...menu };

if (menu.children?.length) {
filteredMenu.children = filterMenus(menu.children, permissions);
// 子菜单全部过滤后,父菜单也不显示
if (filteredMenu.children.length === 0) return acc;
}

acc.push(filteredMenu);
return acc;
}, []);
}

5.3 按钮/元素权限

按钮级权限是最细粒度的前端控制,有三种实现方式。

方式一:Vue 自定义指令 v-permission

directives/permission.ts
import type { Directive, DirectiveBinding } from 'vue';
import { usePermissionStore } from '@/stores/permission';

const permissionDirective: Directive<HTMLElement, string | string[]> = {
mounted(el: HTMLElement, binding: DirectiveBinding<string | string[]>) {
const store = usePermissionStore();
const value = binding.value;
const permissions = Array.isArray(value) ? value : [value];

// 检查修饰符决定匹配模式
const mode = binding.modifiers.some ? 'some' : 'every';

const hasPermission = mode === 'some'
? permissions.some((p) => store.hasPermission(p))
: permissions.every((p) => store.hasPermission(p));

if (!hasPermission) {
// 从 DOM 中移除元素(比 display:none 更安全)
el.parentNode?.removeChild(el);
}
},
};

export default permissionDirective;

// 使用示例:
// <button v-permission="'user:create'">创建用户</button>
// <button v-permission.some="['user:edit', 'user:admin']">编辑</button>

方式二:React 权限 HOC

components/Authorized.tsx
import type { ReactNode, ComponentType } from 'react';
import { usePermission } from '@/hooks/usePermission';

interface AuthorizedProps {
permission: string | string[];
mode?: 'every' | 'some';
fallback?: ReactNode;
children: ReactNode;
}

/** 权限包裹组件 */
function Authorized({
permission,
mode = 'every',
fallback = null,
children,
}: AuthorizedProps): ReactNode {
const { hasPermission } = usePermission();
const permissions = Array.isArray(permission) ? permission : [permission];

const authorized = mode === 'some'
? permissions.some((p) => hasPermission(p))
: permissions.every((p) => hasPermission(p));

return authorized ? children : fallback;
}

/** HOC 版本:包裹整个组件 */
function withPermission<P extends object>(
Component: ComponentType<P>,
permission: string | string[],
): ComponentType<P> {
return function PermissionWrapper(props: P) {
return (
<Authorized permission={permission} fallback={<div>无权限访问</div>}>
<Component {...props} />
</Authorized>
);
};
}

export { Authorized, withPermission };

// 使用示例:
// <Authorized permission="user:create">
// <Button>创建用户</Button>
// </Authorized>
//
// const ProtectedPage = withPermission(AdminPage, 'admin:access');

方式三:React Hook usePermission

hooks/usePermission.ts
import { useCallback, useMemo } from 'react';
import { usePermissionStore } from '@/stores/permission';

interface UsePermissionReturn {
/** 检查单个权限 */
hasPermission: (code: string) => boolean;
/** 检查多个权限(全部满足) */
hasAllPermissions: (codes: string[]) => boolean;
/** 检查多个权限(任一满足) */
hasAnyPermission: (codes: string[]) => boolean;
/** 是否是超级管理员 */
isSuperAdmin: boolean;
/** 用户角色列表 */
roles: string[];
/** 用户路由配置 */
userRoutes: RouteConfig[];
}

function usePermission(): UsePermissionReturn {
const { permissions, roles, userRoutes } = usePermissionStore();

const isSuperAdmin = useMemo(
() => roles.includes('super_admin'),
[roles],
);

const hasPermission = useCallback(
(code: string): boolean => {
// 超级管理员跳过一切权限检查
if (isSuperAdmin) return true;
return permissions.includes(code);
},
[permissions, isSuperAdmin],
);

const hasAllPermissions = useCallback(
(codes: string[]) => codes.every((code) => hasPermission(code)),
[hasPermission],
);

const hasAnyPermission = useCallback(
(codes: string[]) => codes.some((code) => hasPermission(code)),
[hasPermission],
);

return { hasPermission, hasAllPermissions, hasAnyPermission, isSuperAdmin, roles, userRoutes };
}

export { usePermission };
export type { UsePermissionReturn };

六、数据权限

数据权限控制用户能看到哪些数据,粒度比功能权限更细。

6.1 数据权限范围

6.2 行级权限实现

行级权限通过在 SQL 查询时动态注入条件来实现,通常在后端的数据访问层完成。

services/data-permission.ts
interface DataPermissionContext {
userId: string;
departmentId: string;
dataScope: DataScope;
customDepartmentIds?: string[];
}

/** 根据数据权限范围构建 SQL 条件 */
function buildDataScopeCondition(ctx: DataPermissionContext): string {
switch (ctx.dataScope) {
case 'all':
return '1 = 1'; // 不限制

case 'department_and_children':
return `department_id IN (
SELECT id FROM departments
WHERE id = '${ctx.departmentId}'
OR path LIKE '${ctx.departmentId}/%'
)`;

case 'department':
return `department_id = '${ctx.departmentId}'`;

case 'self':
return `created_by = '${ctx.userId}'`;

case 'custom':
const ids = (ctx.customDepartmentIds ?? []).map((id) => `'${id}'`).join(',');
return `department_id IN (${ids})`;

default:
return `created_by = '${ctx.userId}'`; // 默认只看自己
}
}

/** TypeORM 查询示例 */
async function findOrdersWithDataScope(
ctx: DataPermissionContext,
queryBuilder: SelectQueryBuilder<Order>,
): Promise<Order[]> {
const condition = buildDataScopeCondition(ctx);
return queryBuilder
.where(condition)
.orderBy('created_at', 'DESC')
.getMany();
}
SQL 注入风险

上面的示例为了展示原理使用了字符串拼接。在生产环境中必须使用参数化查询来防止 SQL 注入。TypeORM、Prisma 等 ORM 框架本身已经内置了参数化处理。

6.3 列级权限实现

列级权限控制不同角色能看到哪些字段,常见于敏感信息(手机号、身份证)脱敏场景。

utils/column-permission.ts
/** 列级权限配置 */
interface ColumnPermission {
field: string;
/** 需要此权限才能查看原始值 */
requiredPermission: string;
/** 无权限时的脱敏规则 */
maskRule: 'hide' | 'partial' | 'hash';
}

const columnPermissions: ColumnPermission[] = [
{ field: 'phone', requiredPermission: 'data:phone:view', maskRule: 'partial' },
{ field: 'idCard', requiredPermission: 'data:id_card:view', maskRule: 'partial' },
{ field: 'salary', requiredPermission: 'data:salary:view', maskRule: 'hide' },
];

/** 根据权限脱敏数据 */
function maskSensitiveData<T extends Record<string, unknown>>(
data: T,
userPermissions: string[],
): T {
const result = { ...data };

for (const config of columnPermissions) {
if (!(config.field in result)) continue;
if (userPermissions.includes(config.requiredPermission)) continue;

const value = String(result[config.field]);
switch (config.maskRule) {
case 'hide':
result[config.field] = '***' as T[keyof T];
break;
case 'partial':
// 保留首尾字符,中间用 * 替代
result[config.field] = (
value.length > 4
? value.slice(0, 2) + '*'.repeat(value.length - 4) + value.slice(-2)
: '****'
) as T[keyof T];
break;
case 'hash':
result[config.field] = `hash_${btoa(value).slice(0, 8)}` as T[keyof T];
break;
}
}

return result;
}

七、权限 SDK 设计

将权限逻辑封装为独立 SDK,供多个项目复用。

7.1 SDK 核心架构

7.2 SDK 实现

sdk/permission-sdk.ts
type PermissionChangeCallback = (permissions: string[]) => void;

interface PermissionSDKOptions {
/** 获取权限数据的 API */
fetchPermissions: () => Promise<UserPermissionInfo>;
/** 缓存 key */
cacheKey?: string;
/** 缓存过期时间(毫秒) */
cacheTTL?: number;
/** WebSocket URL(可选,用于实时推送) */
wsUrl?: string;
/** 权限变更回调 */
onChange?: PermissionChangeCallback;
}

class PermissionManager {
private permissions: Set<string> = new Set();
private roles: Set<string> = new Set();
private menus: Permission[] = [];
private dataScope: DataScope = 'self';
private options: Required<PermissionSDKOptions>;
private ws: WebSocket | null = null;
private listeners: Set<PermissionChangeCallback> = new Set();
private initialized = false;

constructor(options: PermissionSDKOptions) {
this.options = {
cacheKey: 'permission_cache',
cacheTTL: 30 * 60 * 1000, // 30 分钟
wsUrl: '',
onChange: () => {},
...options,
};
}

/** 初始化:加载权限数据 */
async init(): Promise<void> {
// 1. 尝试从缓存加载
const cached = this.loadFromCache();
if (cached) {
this.applyPermissions(cached);
}

// 2. 从服务端获取最新数据
try {
const data = await this.options.fetchPermissions();
this.applyPermissions(data);
this.saveToCache(data);
} catch (error) {
if (!cached) throw error; // 无缓存时抛错
console.warn('使用缓存权限数据', error);
}

// 3. 建立 WebSocket 连接(如果配置了)
if (this.options.wsUrl) {
this.connectWebSocket();
}

this.initialized = true;
}

/** 检查是否拥有指定权限 */
check(permissionCode: string): boolean {
this.ensureInitialized();
if (this.roles.has('super_admin')) return true;
return this.permissions.has(permissionCode);
}

/** check 的别名 */
hasPermission(code: string): boolean {
return this.check(code);
}

/** 检查是否拥有任一权限 */
hasAnyPermission(codes: string[]): boolean {
return codes.some((code) => this.check(code));
}

/** 检查是否拥有全部权限 */
hasAllPermissions(codes: string[]): boolean {
return codes.every((code) => this.check(code));
}

/** 获取菜单树 */
getMenus(): Permission[] {
return this.menus;
}

/** 获取数据权限范围 */
getDataScope(): DataScope {
return this.dataScope;
}

/** 监听权限变更 */
onPermissionChange(callback: PermissionChangeCallback): () => void {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}

/** 手动刷新权限 */
async refresh(): Promise<void> {
const data = await this.options.fetchPermissions();
this.applyPermissions(data);
this.saveToCache(data);
this.notifyListeners();
}

/** 销毁实例 */
destroy(): void {
this.ws?.close();
this.listeners.clear();
this.permissions.clear();
this.roles.clear();
this.initialized = false;
}

// ========== 私有方法 ==========

private applyPermissions(data: UserPermissionInfo): void {
this.permissions = new Set(data.permissions);
this.roles = new Set(data.roles);
this.menus = data.menus;
this.dataScope = data.dataScope;
}

private loadFromCache(): UserPermissionInfo | null {
try {
const raw = localStorage.getItem(this.options.cacheKey);
if (!raw) return null;
const { data, timestamp } = JSON.parse(raw) as {
data: UserPermissionInfo;
timestamp: number;
};
// 检查缓存是否过期
if (Date.now() - timestamp > this.options.cacheTTL) {
localStorage.removeItem(this.options.cacheKey);
return null;
}
return data;
} catch {
return null;
}
}

private saveToCache(data: UserPermissionInfo): void {
try {
localStorage.setItem(
this.options.cacheKey,
JSON.stringify({ data, timestamp: Date.now() }),
);
} catch {
console.warn('权限缓存写入失败');
}
}

private connectWebSocket(): void {
this.ws = new WebSocket(this.options.wsUrl);

this.ws.onmessage = (event: MessageEvent) => {
const message = JSON.parse(String(event.data)) as { type: string; data: UserPermissionInfo };
if (message.type === 'permission_changed') {
this.applyPermissions(message.data);
this.saveToCache(message.data);
this.notifyListeners();
}
};

this.ws.onclose = () => {
// 断线重连
setTimeout(() => this.connectWebSocket(), 3000);
};
}

private notifyListeners(): void {
const perms = [...this.permissions];
this.listeners.forEach((cb) => cb(perms));
this.options.onChange(perms);
}

private ensureInitialized(): void {
if (!this.initialized) {
throw new Error('PermissionManager 未初始化,请先调用 init()');
}
}
}

// 单例导出
let instance: PermissionManager | null = null;

function createPermissionSDK(options: PermissionSDKOptions): PermissionManager {
if (instance) instance.destroy();
instance = new PermissionManager(options);
return instance;
}

function getPermissionSDK(): PermissionManager {
if (!instance) throw new Error('请先调用 createPermissionSDK 初始化');
return instance;
}

export { PermissionManager, createPermissionSDK, getPermissionSDK };

7.3 React 集成

providers/PermissionProvider.tsx
import { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import type { ReactNode } from 'react';
import { getPermissionSDK, type PermissionManager } from '@/sdk/permission-sdk';

interface PermissionContextValue {
hasPermission: (code: string) => boolean;
hasAnyPermission: (codes: string[]) => boolean;
hasAllPermissions: (codes: string[]) => boolean;
isSuperAdmin: boolean;
loading: boolean;
refresh: () => Promise<void>;
}

const PermissionContext = createContext<PermissionContextValue | null>(null);

function PermissionProvider({ children }: { children: ReactNode }): ReactNode {
const [loading, setLoading] = useState(true);
const [, forceUpdate] = useState(0); // 触发重渲染

const sdk = useMemo(() => getPermissionSDK(), []);

useEffect(() => {
sdk.init().then(() => setLoading(false));

// 监听权限变更,触发子组件重渲染
const unsubscribe = sdk.onPermissionChange(() => {
forceUpdate((n) => n + 1);
});

return () => {
unsubscribe();
};
}, [sdk]);

const hasPermission = useCallback((code: string) => sdk.check(code), [sdk]);
const hasAnyPermission = useCallback((codes: string[]) => sdk.hasAnyPermission(codes), [sdk]);
const hasAllPermissions = useCallback((codes: string[]) => sdk.hasAllPermissions(codes), [sdk]);
const isSuperAdmin = useMemo(() => sdk.check('*'), [sdk]);
const refresh = useCallback(() => sdk.refresh(), [sdk]);

return (
<PermissionContext.Provider
value={{ hasPermission, hasAnyPermission, hasAllPermissions, isSuperAdmin, loading, refresh }}
>
{children}
</PermissionContext.Provider>
);
}

function usePermissionContext(): PermissionContextValue {
const ctx = useContext(PermissionContext);
if (!ctx) throw new Error('usePermissionContext 必须在 PermissionProvider 内使用');
return ctx;
}

export { PermissionProvider, usePermissionContext };

7.4 Vue 集成

plugins/permission-plugin.ts
import type { App, Directive, DirectiveBinding } from 'vue';
import { getPermissionSDK } from '@/sdk/permission-sdk';

/** Vue 插件:注册 v-permission 指令 */
const permissionPlugin = {
install(app: App): void {
const sdk = getPermissionSDK();

// 注册全局指令
const directive: Directive<HTMLElement, string | string[]> = {
mounted(el: HTMLElement, binding: DirectiveBinding<string | string[]>) {
const value = binding.value;
const codes = Array.isArray(value) ? value : [value];
const mode = binding.modifiers.some ? 'some' : 'every';

const authorized = mode === 'some'
? sdk.hasAnyPermission(codes)
: sdk.hasAllPermissions(codes);

if (!authorized) {
el.parentNode?.removeChild(el);
}
},
};

app.directive('permission', directive);

// 注册全局方法
app.config.globalProperties.$hasPermission = (code: string) => sdk.check(code);
},
};

export default permissionPlugin;

// 使用:
// app.use(permissionPlugin);
// <button v-permission="'user:delete'">删除</button>
// <div v-permission.some="['admin', 'editor']">内容</div>

八、权限缓存与同步

8.1 权限数据生命周期

8.2 缓存策略实现

services/permission-cache.ts
/** 多级缓存策略 */
class PermissionCacheService {
/**
* 缓存优先级:内存 > localStorage > 服务端
* 写入时同步写入所有层级
*/
private memoryCache: UserPermissionInfo | null = null;
private readonly CACHE_KEY = 'perm_cache';
private readonly TTL = 30 * 60 * 1000; // 30 分钟

/** 读取权限数据(多级缓存穿透) */
async getPermissions(fetchFn: () => Promise<UserPermissionInfo>): Promise<UserPermissionInfo> {
// Level 1:内存缓存
if (this.memoryCache) return this.memoryCache;

// Level 2:localStorage 缓存
const localData = this.getFromStorage();
if (localData) {
this.memoryCache = localData;
return localData;
}

// Level 3:服务端请求
const serverData = await fetchFn();
this.setPermissions(serverData);
return serverData;
}

/** 写入权限数据(同步到所有缓存层) */
setPermissions(data: UserPermissionInfo): void {
// 写入内存
this.memoryCache = data;
// 写入 localStorage
try {
localStorage.setItem(this.CACHE_KEY, JSON.stringify({
data,
expireAt: Date.now() + this.TTL,
}));
} catch {
// storage 满了,清理旧数据
this.clearStorage();
}
}

/** 清除所有缓存 */
clear(): void {
this.memoryCache = null;
localStorage.removeItem(this.CACHE_KEY);
}

private getFromStorage(): UserPermissionInfo | null {
try {
const raw = localStorage.getItem(this.CACHE_KEY);
if (!raw) return null;
const { data, expireAt } = JSON.parse(raw) as {
data: UserPermissionInfo;
expireAt: number;
};
if (Date.now() > expireAt) {
localStorage.removeItem(this.CACHE_KEY);
return null;
}
return data;
} catch {
return null;
}
}

private clearStorage(): void {
// 清理所有权限相关缓存
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith('perm_')) keysToRemove.push(key);
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
}
}

8.3 WebSocket 实时推送

services/permission-ws.ts
interface PermissionWSOptions {
url: string;
token: string;
onPermissionChange: (data: UserPermissionInfo) => void;
onForceLogout: () => void;
}

class PermissionWebSocket {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;

constructor(private options: PermissionWSOptions) {}

connect(): void {
const { url, token } = this.options;
this.ws = new WebSocket(`${url}?token=${token}`);

this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.startHeartbeat();
};

this.ws.onmessage = (event: MessageEvent) => {
const msg = JSON.parse(String(event.data)) as {
type: string;
data?: UserPermissionInfo;
};

switch (msg.type) {
case 'permission_changed':
// 权限被管理员修改
if (msg.data) this.options.onPermissionChange(msg.data);
break;
case 'role_changed':
// 角色被修改,需要重新拉取完整权限
if (msg.data) this.options.onPermissionChange(msg.data);
break;
case 'force_logout':
// 被踢出登录(账号被禁用、密码被修改等)
this.options.onForceLogout();
break;
}
};

this.ws.onclose = () => {
this.stopHeartbeat();
this.tryReconnect();
};
}

private tryReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => this.connect(), delay);
}

private startHeartbeat(): void {
this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
}

private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}

disconnect(): void {
this.stopHeartbeat();
this.ws?.close();
this.ws = null;
}
}

export { PermissionWebSocket };

九、超级管理员与权限码设计

9.1 超级管理员处理

utils/super-admin.ts
/** 超级管理员判断(多种策略) */
const SUPER_ADMIN_STRATEGIES = {
/** 策略一:特定角色码 */
byRole: (roles: string[]) => roles.includes('super_admin'),

/** 策略二:通配符权限 */
byWildcard: (permissions: string[]) => permissions.includes('*'),

/** 策略三:特定用户 ID */
byUserId: (userId: string) => userId === '1', // 系统初始化的管理员
} as const;

/** 统一的权限检查(内置超管逻辑) */
function checkPermission(
userInfo: { roles: string[]; permissions: string[]; userId: string },
requiredPermission: string,
): boolean {
// 超级管理员直接放行
if (
SUPER_ADMIN_STRATEGIES.byRole(userInfo.roles) ||
SUPER_ADMIN_STRATEGIES.byWildcard(userInfo.permissions)
) {
return true;
}

return userInfo.permissions.includes(requiredPermission);
}
超管安全建议
  • 超级管理员账号应限制登录 IP,仅允许内网访问
  • 超管操作应全量记录审计日志
  • 建议设置二次确认(MFA)机制
  • 不建议用 userId === '1' 这种硬编码方式,应通过角色判断

9.2 权限码设计

权限码是权限的唯一标识,有两种主流设计方案。

方案对比

特性字符串权限码位运算权限码
可读性高(user:create低(0b0100
存储空间较大极小(一个数字存多个权限)
查询性能Set/Map 查找 O(1)位与运算 O(1)
扩展性无限制受位数限制(JS 安全整数 53 位)
适用场景通用后台管理系统简单权限、游戏、嵌入式
组合运算需要遍历数组一条位运算表达式

字符串权限码(推荐)

constants/permission-codes.ts
/**
* 权限码命名规范:{模块}:{资源}:{操作}
* 模块:system, user, order, content 等
* 操作:list, create, update, delete, export, import, approve
*/
const PERMISSION = {
// 用户管理
USER_LIST: 'user:list',
USER_CREATE: 'user:create',
USER_UPDATE: 'user:update',
USER_DELETE: 'user:delete',
USER_EXPORT: 'user:export',
USER_RESET_PWD: 'user:reset_password',

// 角色管理
ROLE_LIST: 'system:role:list',
ROLE_CREATE: 'system:role:create',
ROLE_UPDATE: 'system:role:update',
ROLE_DELETE: 'system:role:delete',
ROLE_ASSIGN: 'system:role:assign',

// 订单管理
ORDER_LIST: 'order:list',
ORDER_DETAIL: 'order:detail',
ORDER_APPROVE: 'order:approve',
ORDER_EXPORT: 'order:export',
} as const;

type PermissionCode = (typeof PERMISSION)[keyof typeof PERMISSION];

位运算权限码

utils/bitwise-permission.ts
/** 位运算权限 - 适合简单场景 */
const BitPermission = {
NONE: 0b00000000, // 0 - 无权限
READ: 0b00000001, // 1 - 读
CREATE: 0b00000010, // 2 - 创建
UPDATE: 0b00000100, // 4 - 更新
DELETE: 0b00001000, // 8 - 删除
EXPORT: 0b00010000, // 16 - 导出
IMPORT: 0b00100000, // 32 - 导入
ADMIN: 0b11111111, // 255 - 全部权限
} as const;

type BitPerm = (typeof BitPermission)[keyof typeof BitPermission];

/** 检查权限(位与运算) */
function hasBitPermission(userPerm: number, requiredPerm: number): boolean {
return (userPerm & requiredPerm) === requiredPerm;
}

/** 添加权限(位或运算) */
function addBitPermission(currentPerm: number, newPerm: number): number {
return currentPerm | newPerm;
}

/** 移除权限(位与 + 取反) */
function removeBitPermission(currentPerm: number, removePerm: number): number {
return currentPerm & ~removePerm;
}

// 使用示例
const editorPerm = BitPermission.READ | BitPermission.CREATE | BitPermission.UPDATE; // 0b00000111 = 7
console.log(hasBitPermission(editorPerm, BitPermission.READ)); // true
console.log(hasBitPermission(editorPerm, BitPermission.DELETE)); // false
console.log(hasBitPermission(BitPermission.ADMIN, BitPermission.EXPORT)); // true(ADMIN 拥有所有权限)
位运算的实用场景

虽然字符串权限码在后台管理系统中更主流,但位运算在某些场景下非常高效:

  • Linux 文件权限chmod 755 就是用位运算表示 rwx
  • Feature Flags:用一个整数存储多个功能开关
  • 游戏权限:快速判断玩家是否拥有某个技能/道具的权限

十、性能优化

10.1 权限数据预加载

utils/preload-permission.ts
/** 在 HTML 中内联权限数据,避免额外请求 */
// 服务端渲染时注入到 HTML:
// <script>window.__PERMISSIONS__ = JSON.stringify(permissionData)</script>

function getPreloadedPermissions(): UserPermissionInfo | null {
if (typeof window !== 'undefined' && (window as { __PERMISSIONS__?: UserPermissionInfo }).__PERMISSIONS__) {
const data = (window as { __PERMISSIONS__?: UserPermissionInfo }).__PERMISSIONS__!;
// 使用后立即清除,避免被恶意脚本读取
delete (window as { __PERMISSIONS__?: UserPermissionInfo }).__PERMISSIONS__;
return data;
}
return null;
}

10.2 权限检查性能优化

utils/permission-optimizer.ts
/** 使用 Set 替代 Array 提升查找性能 */
class OptimizedPermissionChecker {
private permissionSet: Set<string>; // O(1) 查找
private wildcardPatterns: string[]; // 通配符模式

constructor(permissions: string[]) {
this.permissionSet = new Set(permissions);
// 提取通配符权限,如 'user:*' 或 '*'
this.wildcardPatterns = permissions.filter((p) => p.includes('*'));
}

check(code: string): boolean {
// 1. 精确匹配
if (this.permissionSet.has(code)) return true;

// 2. 通配符匹配
return this.wildcardPatterns.some((pattern) => {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
return regex.test(code);
});
}
}

10.3 路由守卫性能优化

router/guard-optimization.ts
import type { Router } from 'vue-router';

/** 白名单路由,无需权限检查 */
const WHITE_LIST = new Set(['/login', '/register', '/404', '/403']);

function setupRouterGuard(router: Router): void {
router.beforeEach(async (to, _from, next) => {
// 1. 白名单直接放行
if (WHITE_LIST.has(to.path)) {
next();
return;
}

// 2. 检查 Token
const token = localStorage.getItem('token');
if (!token) {
next({ path: '/login', query: { redirect: to.fullPath } });
return;
}

// 3. 检查权限数据是否已加载
const permStore = usePermissionStore();
if (!permStore.isLoaded) {
try {
await permStore.loadPermissions();
// 权限加载完成后,动态路由已注册,需要重新导航
next({ ...to, replace: true });
} catch {
// Token 过期或无效
localStorage.removeItem('token');
next({ path: '/login', query: { redirect: to.fullPath } });
}
return;
}

// 4. 检查路由权限
const permission = to.meta?.permission as string | undefined;
if (permission && !permStore.hasPermission(permission)) {
next('/403');
return;
}

next();
});
}

十一、扩展设计

11.1 多租户权限隔离

services/multi-tenant.ts
/** 多租户权限模型 */
interface TenantPermission {
tenantId: string;
userId: string;
roles: string[];
permissions: string[];
}

class MultiTenantPermission {
private tenantPermissions: Map<string, Set<string>> = new Map();

/** 切换租户时更新权限 */
async switchTenant(tenantId: string): Promise<void> {
if (!this.tenantPermissions.has(tenantId)) {
const data = await fetch(`/api/tenant/${tenantId}/permissions`).then((r) => r.json());
this.tenantPermissions.set(tenantId, new Set((data as { permissions: string[] }).permissions));
}
}

/** 检查当前租户下的权限 */
check(tenantId: string, permission: string): boolean {
const perms = this.tenantPermissions.get(tenantId);
return perms?.has(permission) ?? false;
}
}

11.2 微前端权限下发

services/micro-frontend-permission.ts
/** 主应用下发权限给子应用 */
interface MicroAppPermission {
appName: string;
permissions: string[];
menus: Permission[];
}

/** 主应用:按子应用前缀分发权限 */
function distributePermissions(
allPermissions: string[],
appPrefixMap: Record<string, string>, // { 'user-app': 'user:', 'order-app': 'order:' }
): Map<string, string[]> {
const result = new Map<string, string[]>();

for (const [appName, prefix] of Object.entries(appPrefixMap)) {
const appPerms = allPermissions.filter((p) => p.startsWith(prefix));
result.set(appName, appPerms);
}

return result;
}

/** 子应用:接收并初始化权限 */
function initSubAppPermission(permissions: string[]): void {
const sdk = getPermissionSDK();
// 子应用只使用下发的权限子集
sdk.init();
}

11.3 RBAC + ABAC 混合模型

在实际的大型系统中,RBAC 和 ABAC 经常结合使用:RBAC 处理通用功能权限,ABAC 处理基于属性的动态策略。

services/hybrid-permission.ts
interface HybridPermissionContext {
user: UserPermissionInfo;
resource?: { type: string; ownerId: string; department: string };
environment?: { time: Date; ip: string };
}

type PolicyRule = (ctx: HybridPermissionContext) => boolean;

class HybridPermissionEngine {
private rbacPermissions: Set<string> = new Set();
private abacPolicies: Map<string, PolicyRule[]> = new Map();

/** RBAC 基础检查 */
private checkRBAC(permission: string): boolean {
return this.rbacPermissions.has(permission);
}

/** ABAC 策略检查 */
private checkABAC(permission: string, ctx: HybridPermissionContext): boolean {
const policies = this.abacPolicies.get(permission);
if (!policies || policies.length === 0) return true; // 无策略时不限制
return policies.every((policy) => policy(ctx));
}

/** 混合检查:先过 RBAC,再过 ABAC */
check(permission: string, ctx: HybridPermissionContext): boolean {
// Step 1: RBAC 检查(必须拥有基础功能权限)
if (!this.checkRBAC(permission)) return false;
// Step 2: ABAC 检查(附加条件策略)
return this.checkABAC(permission, ctx);
}
}

// 使用示例:
// 1. RBAC 授予 "order:approve" 权限
// 2. ABAC 追加策略:只有工作时间、同部门才能审批

常见面试问题

Q1: 前端权限控制能防住恶意用户吗?

答案

不能。 前端权限控制本质上是用户体验优化,不是安全防线。

层面作用能否被绕过
前端路由权限隐藏无权限页面可以,直接在地址栏输入 URL
前端菜单过滤隐藏侧边栏菜单项可以,通过 DevTools 或直接请求 API
前端按钮隐藏隐藏操作按钮可以,通过 DevTools 或直接调接口
后端接口鉴权拒绝无权限请求不能(只要后端正确实现)
正确的做法:前后端双重校验
// 前端:隐藏按钮(体验优化)
// <button v-permission="'user:delete'">删除</button>

// 后端:接口鉴权(安全保障)
async function deleteUser(req: Request, res: Response): Promise<void> {
const user = req.user;
// 后端必须独立校验权限,不能信任前端
if (!checkPermission(user, 'user:delete')) {
res.status(403).json({ message: '无权限' });
return;
}
// 执行删除逻辑...
}
核心原则

永远不要信任前端的权限控制。 前端代码对用户完全透明,任何客户端校验都可以被绕过。安全的权限校验必须在后端完成。前端权限只是为了减少无效请求提升用户体验

Q2: RBAC 和 ABAC 的区别是什么?什么时候用 ABAC?

答案

维度RBACABAC
决策依据用户的角色用户/资源/环境的属性组合
灵活度中等非常高
实现复杂度
管理方式角色-权限映射表策略规则引擎
典型场景后台管理系统云平台 IAM、医疗系统
ABAC 适用场景示例
// 场景:医生只能在上班时间、从医院内网访问患者病历
// RBAC 无法表达这种"条件性权限"

const medicalRecordPolicy: ABACPolicy = (ctx) => {
const { user, resource, environment } = ctx;

return (
user.roles.includes('doctor') && // 必须是医生
user.department === resource.department && // 同科室
environment.time.getHours() >= 8 && environment.time.getHours() <= 20 && // 工作时间
environment.ip.startsWith('10.0.') // 内网 IP
);
};

// 实际开发中 RBAC 和 ABAC 经常结合使用:
// - RBAC 处理 90% 的通用权限(菜单、按钮)
// - ABAC 处理 10% 的特殊条件权限(时间、地点、数据属性)

Q3: 按钮级权限怎么实现?有哪些方案?

答案

三种主流方案各有优缺点。

方案实现方式优点缺点
v-if / 条件渲染v-if="hasPermission('user:delete')"简单直接到处写判断,代码冗余
自定义指令v-permission="'user:delete'"声明式,干净只能控制 DOM 移除,不支持复杂逻辑
HOC / 包裹组件Authorized permission="..."灵活,支持 fallback多一层嵌套,稍显冗余
三种方案的使用对比
// 方案 1: v-if 条件渲染(Vue)
// <button v-if="hasPermission('user:delete')">删除</button>

// 方案 2: 自定义指令(Vue)
// <button v-permission="'user:delete'">删除</button>

// 方案 3: 包裹组件(React)
function UserActions(): React.ReactElement {
return (
<div>
<Authorized permission="user:create">
<Button type="primary">创建</Button>
</Authorized>
<Authorized permission="user:delete" fallback={<Button disabled>删除(无权限)</Button>}>
<Button danger>删除</Button>
</Authorized>
</div>
);
}
最佳实践
  • Vue 项目推荐自定义指令 v-permission,简洁且符合 Vue 的声明式风格
  • React 项目推荐 Hook + 组件结合:usePermission 用于逻辑判断,Authorized 组件用于 UI 控制
  • 对于需要禁用而非隐藏的场景(如展示"无权限"提示),使用包裹组件的 fallback 更合适

Q4: 权限变更后如何实时生效?

答案

权限变更实时生效需要解决两个问题:(1) 通知前端权限已变更;(2) 前端收到通知后更新 UI。

完整的权限实时更新方案
/** 三种通知机制 */
// 1. WebSocket 推送(推荐,实时性最好)
// 管理员修改权限 --> 后端发 WS 消息 --> 前端更新权限 + 重新渲染

// 2. 轮询检查(简单但有延迟)
setInterval(async () => {
const { version } = await fetch('/api/permission/version').then((r) => r.json()) as { version: number };
if (version > currentVersion) {
await permissionSDK.refresh();
}
}, 60000); // 每分钟检查一次

// 3. Token 刷新时顺带更新(折中方案)
async function refreshToken(): Promise<void> {
const { token, permissions } = await fetch('/api/token/refresh').then((r) => r.json()) as {
token: string;
permissions: UserPermissionInfo;
};
localStorage.setItem('token', token);
permissionSDK.refresh(); // 顺带刷新权限
}

前端收到权限变更后,需要响应式地更新 UI

React 中权限变更自动更新 UI
function AdminPage(): React.ReactElement {
const { hasPermission } = usePermission();
// hasPermission 基于 state/store,权限变更会触发组件重渲染

return (
<div>
{hasPermission('user:create') && <Button>创建用户</Button>}
{hasPermission('user:delete') && <Button danger>删除用户</Button>}
</div>
);
}
方案实时性实现成本服务端压力
WebSocket 推送毫秒级
轮询分钟级
Token 刷新取决于 Token 有效期
页面刷新用户手动

相关链接