角色 vs 权限:为什么你的 RBAC 不应该使用角色检查
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
}
- 定义权限(
VIEW、CREATE、UPDATE、DELETE)。 - 在 一个地方 将角色映射到权限。
- 检查权限,而不是角色。
- 添加新角色 = 只需更新映射表。
结论
检查一下你当前的代码库:有多少地方出现了 user.role === '…'?这些地方在你添加新角色时都会出问题。现在就重构为基于权限的检查吧,等产品规模扩大时,你会感谢自己的决定。