设计权限管理系统
问题
如何设计一个完善的前后端权限管理系统?从权限模型选型、前端三层权限控制、权限 SDK 设计到数据权限与缓存同步,请详细说明核心模块的设计思路与关键技术实现。
答案
权限管理系统是中后台应用的基础设施,需要解决"谁能在什么条件下对哪些资源执行什么操作"这一核心问题。一个完整的权限系统涉及权限模型设计、前端多层控制(路由/菜单/按钮)、后端鉴权、数据权限、权限 SDK、缓存同步等多个关键模块。前端权限控制是用户体验的保障,后端鉴权才是安全的底线。
一、需求分析
功能需求
| 模块 | 功能点 |
|---|---|
| 用户管理 | 用户增删改查、批量导入、状态启停 |
| 角色管理 | 角色 CRUD、角色继承、角色分配 |
| 权限分配 | 菜单权限、按钮权限、API 权限、数据权限 |
| 组织架构 | 部门管理、岗位管理、用户-部门-角色关联 |
| 权限校验 | 前端路由/菜单/按钮控制、后端接口鉴权 |
| 审计日志 | 操作日志、权限变更记录、登录日志 |
非功能需求
| 指标 | 目标 |
|---|---|
| 安全性 | 前后端双重校验,防越权访问 |
| 性能 | 权限校验 < 1ms,不阻塞正常业务请求 |
| 灵活性 | 支持动态调整,权限变更实时生效 |
| 可扩展 | 支持多租户、多系统、微前端权限隔离 |
| 易用性 | SDK 提供 React Hooks / Vue 指令等开箱即用的 API |
前端权限控制是体验优化(隐藏无权限的 UI),后端权限校验是安全保障(拦截无权限的请求)。两者缺一不可,但安全边界永远在后端。
二、权限模型演进
权限模型经历了从简单到复杂的演进过程,理解各模型的适用场景是系统设计的基础。
2.1 四种权限模型对比
| 模型 | 全称 | 核心思想 | 典型场景 |
|---|---|---|---|
| DAC | Discretionary Access Control | 资源所有者自行决定谁能访问 | 文件系统、Google Docs 共享 |
| MAC | Mandatory Access Control | 系统强制分配安全等级,不可自行变更 | 军事系统、政府机密文件 |
| RBAC | Role-Based Access Control | 通过角色间接关联用户和权限 | 企业管理后台、SaaS 系统 |
| ABAC | Attribute-Based Access Control | 基于属性(用户/资源/环境)动态决策 | 云平台 IAM、复杂多租户 |
2.2 模型演进关系
2.3 各模型代码示例
// ========================
// 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 类型定义
/** 权限类型 */
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 角色继承实现
角色继承允许子角色自动拥有父角色的所有权限,减少重复配置。
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 路由权限 - 动态路由
动态路由是前端权限的第一道防线。有两种主流方案。
方案对比
| 特性 | 前端全量路由过滤 | 后端返回路由表 |
|---|---|---|
| 实现复杂度 | 低 | 中 |
| 灵活性 | 中(需前端发版) | 高(后端动态配置) |
| 安全性 | 低(前端包含全部路由信息) | 中(路由信息由后端控制) |
| 维护成本 | 前端维护 | 前后端都需维护 |
| 适用场景 | 中小项目、路由固定 | 大型项目、路由频繁变更 |
方案一:前端全量路由过滤
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 动态注册
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 动态路由方案
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 菜单权限 - 侧边栏过滤
菜单权限基于路由过滤结果,渲染用户可见的侧边栏菜单。
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
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
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
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 查询时动态注入条件来实现,通常在后端的数据访问层完成。
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 注入。TypeORM、Prisma 等 ORM 框架本身已经内置了参数化处理。
6.3 列级权限实现
列级权限控制不同角色能看到哪些字段,常见于敏感信息(手机号、身份证)脱敏场景。
/** 列级权限配置 */
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 实现
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 集成
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 集成
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 缓存策略实现
/** 多级缓存策略 */
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 实时推送
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 超级管理员处理
/** 超级管理员判断(多种策略) */
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 位) |
| 适用场景 | 通用后台管理系统 | 简单权限、游戏、嵌入式 |
| 组合运算 | 需要遍历数组 | 一条位运算表达式 |
字符串权限码(推荐)
/**
* 权限码命名规范:{模块}:{资源}:{操作}
* 模块: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];
位运算权限码
/** 位运算权限 - 适合简单场景 */
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 权限数据预加载
/** 在 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 权限检查性能优化
/** 使用 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 路由守卫性能优化
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 多租户权限隔离
/** 多租户权限模型 */
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 微前端权限下发
/** 主应用下发权限给子应用 */
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 处理基于属性的动态策略。
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?
答案:
| 维度 | RBAC | ABAC |
|---|---|---|
| 决策依据 | 用户的角色 | 用户/资源/环境的属性组合 |
| 灵活度 | 中等 | 非常高 |
| 实现复杂度 | 低 | 高 |
| 管理方式 | 角色-权限映射表 | 策略规则引擎 |
| 典型场景 | 后台管理系统 | 云平台 IAM、医疗系统 |
// 场景:医生只能在上班时间、从医院内网访问患者病历
// 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:
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 有效期 | 低 | 低 |
| 页面刷新 | 用户手动 | 无 | 低 |
相关链接
- OWASP 访问控制 - 访问控制安全最佳实践
- NIST RBAC 标准 - RBAC 标准定义
- XACML (ABAC 标准) - ABAC 策略语言规范
- Casbin - 开源访问控制库(支持 RBAC/ABAC/ACL)
- CASL - JavaScript 同构权限库
- Vue Router 导航守卫 - Vue 路由权限实现基础
- React Router - React 路由权限实现基础