当前位置: 首页 > news >正文

【Nest】权限管理——RBAC/CASL

一、RBAC vs CASL(对比与适用场景)

RBAC(Role-Based Access Control)

  • 思路:给用户分配角色(如 admin, editor, viewer),再给角色分配权限(或直接在角色层面判断)。
  • 优点:易管理、便于审计、适合权限较少、粒度较粗的场景。
  • 缺点:无法灵活表达对象级别或属性级别的细粒度规则(比如“用户可以编辑自己发布的文章”)除非在代码里加额外判断。

CASL(能力/能力表述,Capability-based)

  • 思路:用“能力”(Ability)来表达“谁能对哪个资源做什么(action + subject)”,可以非常细粒度(支持条件、字段限制、对象级判断)。
  • 优点:灵活,支持条件/属性级授权(例如:can('update', 'Article', { authorId: user.id }))。
  • 缺点:比RBAC复杂,需要把能力从角色或其他来源构造出来(通常在 AbilityFactory 中基于用户角色/权限或其他属性动态构造 Ability)。

常见组合方式

  • 用 RBAC 管理“粗粒度”的模块级/页面级权限(如是否能进入管理页面),用 CASL 做资源级/字段级的细粒度检查(如是否能编辑 / 删除某条数据)。
  • 或者把“角色”映射到 CASL 能力(AbilityFactory 从角色生成 ability)。

二、数据库模型(关系型,示例使用 PostgreSQL / MySQL,配合 TypeORM)

下面给出表结构与关系(ER 概念),以及 TypeORM 实体草稿。

ER 概要

  • users (1) — (M) user_roles — (M) roles
  • roles (1) — (M) role_permissions — (M) permissions
  • 也可直接 users -> user_permissions(当需要给用户直接授予特定 permission)
  • 资源表示例:articles(用于演示对象级权限)

SQL 表定义(简化版)

-- users
CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(100) NOT NULL UNIQUE,email VARCHAR(255) UNIQUE,password_hash VARCHAR(255) NOT NULL,is_active BOOLEAN DEFAULT TRUE,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);-- roles
CREATE TABLE roles (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50) NOT NULL UNIQUE,description TEXT
);-- permissions
CREATE TABLE permissions (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(100) NOT NULL UNIQUE, -- e.g., 'article.create', 'article.update'description TEXT
);-- user_roles (many-to-many)
CREATE TABLE user_roles (user_id BIGINT NOT NULL,role_id BIGINT NOT NULL,PRIMARY KEY (user_id, role_id)
);-- role_permissions (many-to-many)
CREATE TABLE role_permissions (role_id BIGINT NOT NULL,permission_id BIGINT NOT NULL,PRIMARY KEY (role_id, permission_id)
);-- 也可以直接对用户授予权限
CREATE TABLE user_permissions (user_id BIGINT NOT NULL,permission_id BIGINT NOT NULL,PRIMARY KEY (user_id, permission_id)
);-- Example resource: articles
CREATE TABLE articles (id BIGINT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(255),content TEXT,author_id BIGINT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

TypeORM 实体(简化示例)

下面只展示关键字段与关系,省略装饰器配置的细节(但足够用于理解):

// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Role } from './role.entity';@Entity('users')
export class User {@PrimaryGeneratedColumn()id: number;@Column({ unique: true })username: string;@Column()passwordHash: string;@ManyToMany(() => Role, role => role.users)@JoinTable({ name: 'user_roles', joinColumn: { name: 'user_id' }, inverseJoinColumn: { name: 'role_id' } })roles: Role[];// 可选:直接 permissions// @ManyToMany(() => Permission)// @JoinTable({ name: 'user_permissions' })// permissions: Permission[];
}// role.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { User } from './user.entity';
import { Permission } from './permission.entity';@Entity('roles')
export class Role {@PrimaryGeneratedColumn()id: number;@Column({ unique: true })name: string;@ManyToMany(() => User, user => user.roles)users: User[];@ManyToMany(() => Permission, perm => perm.roles)@JoinTable({ name: 'role_permissions', joinColumn: { name: 'role_id' }, inverseJoinColumn: { name: 'permission_id' } })permissions: Permission[];
}// permission.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { Role } from './role.entity';@Entity('permissions')
export class Permission {@PrimaryGeneratedColumn()id: number;@Column({ unique: true })name: string; // 'article.create' etc.@ManyToMany(() => Role, role => role.permissions)roles: Role[];
}// article.entity.ts (用于演示对象级控制)
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';@Entity('articles')
export class Article {@PrimaryGeneratedColumn()id: number;@Column()title: string;@Column('text')content: string;@Column()authorId: number;
}

三、RBAC:@Roles() 装饰器 + RolesGuard(基于 Guard 的实现)

这是比较常见的实现:在 Controller/Route 上写 @Roles('admin','editor'),Guard 从 request.user 读取角色并核对。

1) 自定义装饰器 @Roles()

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

2) RolesGuard

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';@Injectable()
export class RolesGuard implements CanActivate {constructor(private reflector: Reflector) {}async canActivate(context: ExecutionContext): Promise<boolean> {// 使用 UseGuards的类,内部方法使用 @Roles 装饰器// 则 getAllAndOverride 在方法的角色会将外层类的角色覆盖,如果使用 getAllAndMerge 则会合并两个角色const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [context.getHandler(),context.getClass(),]);if (!requiredRoles || requiredRoles.length === 0) {// 没声明角色,则默认允许(或改为默认 deny)return true;}const request = context.switchToHttp().getRequest();const user = request.user;if (!user) {throw new ForbiddenException('用户未登录或未找到 user');}// user.roles 需在 auth 中间件/策略里注入(例如 JWT 验证后查到用户并附带角色)const userRoles: string[] = (user.roles || []).map((r: any) => (typeof r === 'string' ? r : r.name));const has = requiredRoles.some(role => userRoles.includes(role));if (!has) {throw new ForbiddenException('权限不足');}return true;}
}

3) 使用方式(Controller 示例)

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
import { JwtAuthGuard } from './jwt-auth.guard'; // 假设你有 jwt guard@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('admin')
export class AdminController {@Roles('admin')@Get('stats')getStats() {return { ok: true, stats: {} };}
}

注意:

  • JwtAuthGuard 应该把 request.user 填充为包含 roles 的对象(通常在 JWT payload 或在 Guard/Strategy 里从 DB 查询填充)。
  • RolesGuard 适合“角色集合匹配”的场景,但无法表达“只能修改自己的帖子”这种对象级权限。

四、CASL(基于能力的授权)实现(在 NestJS 中常见模式)

下面展示一个基于 @casl/ability(v5+)的实现:AbilityFactory、@CheckAbilities() 装饰器与 PoliciesGuard。Ability 可以基于用户的 roles/permissions 动态构建。

需要安装:npm i @casl/ability @casl/ability-for-nestjs(这里给出通用实现,不依赖 casl 的 Nest 插件)

1) Ability Types

// ability.factory.ts
import { Injectable } from '@nestjs/common';
import { Ability, AbilityBuilder, AbilityClass, ExtractSubjectType, InferSubjects } from '@casl/ability';
import { Article } from './entities/article.entity';// 定义 Action 和 Subjects
export type Actions = 'manage' | 'create' | 'read' | 'update' | 'delete';
export type Subjects = InferSubjects<typeof Article> | 'Article' | 'all';export type AppAbility = Ability<[Actions, Subjects]>;@Injectable()
export class AbilityFactory {createForUser(user: any) {const { can, cannot, build } = new AbilityBuilder<Ability<[Actions, Subjects]>>(Ability as AbilityClass<AppAbility>);if (!user) {// 未认证用户,默认只读公开资源can('read', 'Article');return build();}// 基于角色或 permissions 构造 abilityconst roles = (user.roles || []).map(r => (typeof r === 'string' ? r : r.name));if (roles.includes('admin')) {can('manage', 'all'); // admin 管理所有return build();}// 基于角色到能力的映射(可扩展)if (roles.includes('editor')) {can('create', 'Article');can('read', 'Article');can('update', 'Article');}// 示例:如果用户是文章作者,则允许 update/delete 自己的文章can('update', 'Article', { authorId: user.id });can('delete', 'Article', { authorId: user.id });// 若有 permissions 字段也可遍历赋权if (user.permissions) {for (const perm of user.permissions) {// 假设 perm.name: 'article.create'const [subject, action] = perm.name.split('.');can(action as Actions, subject[0].toUpperCase() + subject.slice(1));}}return build({// 需要暴露 subjectType 的检测方式detectSubjectType: item => (typeof item === 'string' ? item : (item.constructor as ExtractSubjectType<any>)),});}
}

2) 自定义装饰器 @CheckAbilities()

// check-abilities.decorator.ts
import { SetMetadata } from '@nestjs/common';export const CHECK_ABILITY = 'check_ability';
export const CheckAbilities = (...requirements: Array<{ action: string; subject: any }>) =>SetMetadata(CHECK_ABILITY, requirements);

3) PoliciesGuard(读取 Ability 并检查)

// policies.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { CHECK_ABILITY } from './check-abilities.decorator';
import { AbilityFactory } from './ability.factory';
import { AppAbility } from './ability.factory';
import { ForbiddenError } from '@casl/ability';@Injectable()
export class PoliciesGuard implements CanActivate {constructor(private reflector: Reflector, private abilityFactory: AbilityFactory) {}canActivate(context: ExecutionContext): boolean {const requirements = this.reflector.getAllAndOverride<Array<{ action: string; subject: any }>>(CHECK_ABILITY, [context.getHandler(),context.getClass(),]);if (!requirements || requirements.length === 0) {return true;}const request = context.switchToHttp().getRequest();const user = request.user;const ability: AppAbility = this.abilityFactory.createForUser(user);try {for (const req of requirements) {// req.subject 可能是字符串 'Article' 或 Article 实例const subject = req.subject;if (!ability.can(req.action as any, subject)) {// 使用 casl 的 ForbiddenError 可以提供更好信息throw new ForbiddenException('权限不足');}}return true;} catch (err) {if (err instanceof ForbiddenError || err instanceof ForbiddenException) {throw new ForbiddenException('权限不足');}throw err;}}
}

4) 在 Controller 中使用 CASL

import { Controller, Get, UseGuards, Param } from '@nestjs/common';
import { CheckAbilities } from './check-abilities.decorator';
import { PoliciesGuard } from './policies.guard';
import { JwtAuthGuard } from './jwt-auth.guard';
import { ArticlesService } from './articles.service';@UseGuards(JwtAuthGuard, PoliciesGuard)
@Controller('articles')
export class ArticlesController {constructor(private articlesService: ArticlesService) {}@CheckAbilities({ action: 'read', subject: 'Article' })@Get()findAll() {return this.articlesService.findAll();}@CheckAbilities({ action: 'update', subject: 'Article' })@Get(':id')async updateExample(@Param('id') id: string) {// 这里更实际的做法是:// 获取文章实例 -> ability.can('update', articleInstance) —— 对象级检查return {};}
}

对象级检查:通常需要在 Guard 或服务中先拿到具体实例(如 article),然后用 ability.can(action, article) 来判断,因为条件(如 { authorId: user.id })需要具体对象的字段。

示例对象级检查(在 Controller Action 中):

@Get(':id')
async getOne(@Param('id') id: string, @Req() req) {const article = await this.articlesService.findOne(+id);const ability = this.abilityFactory.createForUser(req.user);if (!ability.can('read', article)) {throw new ForbiddenException();}return article;
}

也可以把“对象预取 + casl 检查”逻辑封装成拦截器或自定义 Guard。

http://www.dtcms.com/a/481711.html

相关文章:

  • 使用LSTM进行人类活动识别
  • 列表标签之有序标签(本文为个人学习笔记,内容整理自哔哩哔哩UP主【非学者勿扰】的公开课程。 > 所有知识点归属原作者,仅作非商业用途分享)
  • AI时代BaaS | 开源的后端即服务(BaaS)平台Supaba
  • 达梦存储结构篇
  • 桂林网站制作网站佛山公共交易资源平台
  • 域名验证网站如何找推广平台
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段-二阶段(1):文法運用
  • C++面向对象进阶:从构造函数到static成员
  • python3GUI--模仿百度网盘的本地文件管理器 By:PyQt5(详细分享)
  • Go 1.21 新特性:context.AfterFunc 的实用指南
  • Acer软件下载
  • 分组密码:加密模式、可鉴别加密模式、加密认证模式
  • RocketMQ高并发编程技巧(二)
  • 算法沉淀第二天(Catching the Krug)
  • redis-4.0.11-1.ky10.sw_64.rpm安装教程(申威麒麟V10 64位系统详细步骤)
  • 为企业为什么做网站企业网站建设注意什么
  • 从监听风险到绝对隐私:Zoom偷听门后,Briefing+CPolar重新定义远程会议安全标准
  • 网站源代码下载工具网站备案网站前置审批
  • 基于GENESIS64核心可视化组件GraphWorX64的工业图形设计解决方案
  • QML学习笔记(三十七)QML的Slider
  • 3:Django-migrate
  • 【Linux】网络基础概念
  • Go语言技术与应用(三):服务注册发现机制详解
  • 网线学习笔记
  • 【OpenHarmony】存储管理服务模块架构
  • 网站做报表网站维护是谁做的
  • 阿里云k8s部署微服务yaml和Dockerfile文件脚本
  • [Backstage] 后端插件 | 包架构 | 独立微服务 | by HTTP路由
  • java微服务-尚医通-编写接口
  • Go|sync.Pool|临时对象池,实现临时对象的复用,降低GC压力