角色 vs 权限:为什么你的 RBAC 不应该使用角色检查

发布: (2025年12月11日 GMT+8 21:45)
6 min read
原文: Dev.to

Source: Dev.to

你正在构建一个多用户应用。你添加了一个可以删除产品的管理员和一个不能删除的销售代表。你的代码如下:

// Bad approach – checking roles directly
if (user.role === 'admin') {
  await deleteProduct(id);
} else {
  throw new ForbiddenException('Only admins can delete');
}

这段代码可以工作……直到你的客户问:“我可以再添加一个经理角色吗?他们应该能够删除产品,但不能管理用户。
现在你陷入困境,因为你的代码检查的是 角色,而不是 权限

角色检查的问题

  • 可扩展性 – 每新增一个角色,都必须在代码库中搜索 user.role === '…' 的检查。
  • 可维护性 – 为角色更改权限意味着要编辑很多地方,容易引入回归。
  • 粒度 – 角色是宽泛的;权限是细粒度的(例如 “查看报告” 与 “删除销售”)。

为什么权限更好

与其问 “你的角色是什么?” 不如问 “你是否拥有执行此操作的权限?”
权限描述的是 行为,而不是身份。任何人都可以拥有任意组合的权限。

定义权限

// constants/permissions.ts
export enum Permission {
  // Products
  VIEW_PRODUCTS = 'VIEW_PRODUCTS',
  CREATE_PRODUCT = 'CREATE_PRODUCT',
  UPDATE_PRODUCT = 'UPDATE_PRODUCT',
  DELETE_PRODUCT = 'DELETE_PRODUCT',

  // Sales
  VIEW_SALES = 'VIEW_SALES',
  CREATE_SALE = 'CREATE_SALE',
  DELETE_SALE = 'DELETE_SALE',

  // Admin
  INVITE_USERS = 'INVITE_USERS',
  VIEW_ACTIVITY_LOGS = 'VIEW_ACTIVITY_LOGS',
}

将角色映射到权限

export const ROLE_PERMISSIONS: Record<string, Permission[]> = {
  admin: [
    Permission.VIEW_PRODUCTS,
    Permission.CREATE_PRODUCT,
    Permission.UPDATE_PRODUCT,
    Permission.DELETE_PRODUCT,
    Permission.VIEW_SALES,
    Permission.CREATE_SALE,
    Permission.DELETE_SALE,
    Permission.INVITE_USERS,
    Permission.VIEW_ACTIVITY_LOGS,
  ],

  sales_rep: [
    Permission.VIEW_PRODUCTS,
    Permission.CREATE_PRODUCT,
    Permission.VIEW_SALES,
    Permission.CREATE_SALE,
  ],
};

辅助函数

export function hasPermission(
  role: UserRole,
  permission: Permission,
): boolean {
  return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
}

现在角色只是 权限的集合。添加新角色只需要更新映射表。

重构为权限检查

之前(角色检查)

if (user.role === 'admin') {
  await deleteProduct(id);
}

之后(权限检查)

if (hasPermission(user.role, Permission.DELETE_PRODUCT)) {
  await deleteProduct(id);
}

你可以将这段逻辑封装在守卫和装饰器中,以获得干净的控制器代码。

权限守卫

// common/guards/permissions.guard.ts
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.get<string[]>(
      'permissions',
      context.getHandler(),
    );

    if (!required) return true;

    const { user } = context.switchToHttp().getRequest();

    return required.every(p => hasPermission(user.role, p));
  }
}

控制器示例

@Controller('products')
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class ProductsController {
  @Post()
  @RequirePermission(Permission.CREATE_PRODUCT)
  create(@Body() dto: CreateProductDto) {
    return this.productsService.create(dto);
  }

  @Delete(':id')
  @RequirePermission(Permission.DELETE_PRODUCT) // Only admins have this
  remove(@Param('id') id: string) {
    return this.productsService.remove(id);
  }
}

代码现在是 自解释的:每个端点都明确声明了所需的权限。

管理角色和权限

  • 添加经理角色 – 只需在 ROLE_PERMISSIONS 中扩展:

    manager: [
      Permission.VIEW_PRODUCTS,
      Permission.UPDATE_PRODUCT,
      Permission.DELETE_PRODUCT,
      Permission.VIEW_SALES,
    ],
  • 为已有角色授予新权限 – 修改该角色对应的数组(例如,给 sales_rep 添加 UPDATE_PRODUCT 权限)。无需更改控制器,也不必在代码库中搜索。

测试权限逻辑

it('should allow admin to delete product', () => {
  const canDelete = hasPermission('admin', Permission.DELETE_PRODUCT);
  expect(canDelete).toBe(true);
});

it('should deny sales_rep from deleting product', () => {
  const canDelete = hasPermission('sales_rep', Permission.DELETE_PRODUCT);
  expect(canDelete).toBe(false);
});

权限检查现在是孤立的,易于测试。

何时使用角色检查 vs. 权限检查

使用角色检查的情形

  • 你只有 1–2 个固定角色,且不会改变。
  • 应用 非常简单(例如个人博客或待办事项列表)。
  • 正在构建 原型/MVP,计划后期重构。

使用权限检查的情形

  • 业务需求 可能会变化(它们总是会变)。
  • 你拥有 超过 2 个角色
  • 项目是 企业级或生产级 应用。
  • 客户可能会请求 新角色 或修改功能。

如果不确定,直接使用权限——它们比以后重构基于角色的代码更容易演进。

快速参考

❌ 错误示例:

if (user.role === 'admin') {
  // do thing
}

✅ 正确示例:

if (hasPermission(user.role, Permission.DO_THING)) {
  // do thing
}
  • 定义权限(VIEWCREATEUPDATEDELETE)。
  • 一个地方 将角色映射到权限。
  • 检查权限,而不是角色。
  • 添加新角色 = 只需更新映射表。

结论

检查一下你当前的代码库:有多少地方出现了 user.role === '…'?这些地方在你添加新角色时都会出问题。现在就重构为基于权限的检查吧,等产品规模扩大时,你会感谢自己的决定。

Back to Blog

相关文章

阅读更多 »