Nestjs框架: 策略的权限控制(ACL)与数据权限实战
概述
- 我们的核心目标在于对传统 RBAC(基于角色的访问控制)模型进行深度扩展
- 从而实现更细粒度、更具灵活性的权限管理体系,该体系不仅支持接口级别的访问控制
- 还进一步延伸至数据库字段级操作权限,并引入动态策略判断机制
- 使权限决策过程能够结合运行时上下文数据进行实时评估
权限模型的精细化的扩展:从 RBAC 到策略驱动
- 为提升权限控制的精确性,系统在原有角色-权限映射基础上引入了 Prisma 与 TypeORM 兼容的策略权限框架,并通过 caslability 库实现多模式策略定义。该库支持三种主要策略模式:
- 条件模式(Condition-based):依据预设条件表达式判断是否授权
- 字段级控制模式(Field-level):针对实体特定字段设定读写权限
- 函数式策略模式(Function-based):通过可执行函数动态计算权限结果
- 其中,函数式策略尤其适用于复杂业务场景,如“用户仅能更新自己发布的文章”这类基于主体与资源关系的判断逻辑
- 此类策略需在执行时传入上下文参数(如当前用户、请求对象、目标资源),因此对调用链路中的参数传递和解析提出了更高要求
多数据源兼容的设计与实现:MongoDB 与 Prisma 的双轨支持
- 为适配不同持久化方案,系统分别实现了对 MongoDB 和 Prisma ORM 的策略查询支持
- 二者在初始化方式上存在显著差异:
- 对于 MongoDB 场景,采用工厂方法 createMongoAbility() 构建能力实例;
- 对于 Prisma 或通用对象查询场景,则使用 PureAbility 实例化并配合 buildMongoQueryMatcher 工厂函数建立查询匹配器
- 关键实现细节在于:通过导入 @casl/ability/extra 中的 matchConditions 方法,将 CASL 定义的能力条件与 MongoDB 查询操作符(如 $in, $gt, $eq 等)建立语义映射关系
- 此举使得开发者可以使用类似 MongoDB 原生语法的方式编写权限规则,同时保持与 ORM 抽象层的良好兼容性
示例
// 示例:构建支持 MongoDB 操作符的能力匹配器
import { buildMongoQueryMatcher } from '@casl/ability/extra';
import { matchConditions } from '@casl/ability';const customMatcher = buildMongoQueryMatcher({...matchConditions,// 可扩展自定义操作符
});const ability = new PureAbility(permissions, { anyAction: 'manage', anySubject: 'all' });
- 此设计极大增强了策略表达能力,允许在权限规则中直接使用嵌套查询、数组包含、范围比较等高级逻辑,从而应对复杂的业务规则需求
PolicyGuard 核心鉴权流程的双重循环机制
- 整个策略权限系统的核心落脚点在于 PolicyGuard 的权限校验逻辑,其实现采用了双重迭代结构以完成“接口所需权限”与“用户实际拥有能力”的匹配验证。
具体流程如下:
- 首先,根据目标接口元数据(通过装饰器声明的 @Permission)查询出其所依赖的一个或多个 PermissionPolicy 记录;
- 接着,基于当前用户的 roleID 查询其被分配的所有策略权限记录,并将其转换为一组 Ability 实例集合;进入双重循环比对阶段:
- 外层循环遍历接口所需的每一个权限策略(左侧 policy 数组);
- 内层循环遍历用户所拥有的每一个 Ability 实例(右侧 ability 数组);
- 使用 ability.can(action, subject) 方法尝试匹配当前 policy 所需的动作与资源类型;
- 若任一 Ability 成功匹配当前 policy,则视为该 policy 条件满足,跳出内层循环处理下一个 policy;
- 最终判定标准是:所有接口所需 policy 均被成功匹配;若存在任一 policy 无法被任何 Ability 满足,则拒绝访问
示例
// 简化后的 PolicyGuard 核心逻辑示意
for (const requiredPolicy of interfacePolicies) {let isSatisfied = false;for (const userAbility of userAbilities) {if (userAbility.can(requiredPolicy.action, requiredPolicy.subject)) {isSatisfied = true;break;}}if (!isSatisfied) return false; // 拒绝访问
}
return true; // 授权通过
这一机制确保了权限判断的完整性与严谨性,避免因部分匹配而导致越权风险
Subject 类型映射与 ClassTransformer 的集成优化
- 在策略执行过程中,一个关键技术难点是如何将数据库中存储的字符串形式的 subject(如 “Post”, “User”)与实际的 TypeScript 类(Class)建立关联。
- 由于 CASL 要求 can 方法中的 subject 参数必须为类构造函数或实例,而非字符串标识符,因此必须进行类型映射
- 解决方案是在模块初始化阶段构建一个 subject 字符串到 Class 的映射表(Map),并在 PolicyGuard 中通过 .map() 方法完成转换。例如:
示例
const subjectMap = new Map<string, any>([['Post', PostEntity],['User', UserEntity],['Comment', CommentEntity],
]);// 在创建 Ability 时使用映射
const abilities = policies.map(policy => ({action: policy.action, subject: subjectMap.get(policy.subjectName),
}));
-
随后结合 class-transformer 提供的 plainToClass 方法,将从数据库读取的原始策略配置转化为具备完整类型信息的对象实例,从而保障 Ability 实例在调用 can 方法时能正确识别目标资源类型
-
特别注意:该映射逻辑必须在模块范围内统一管理,不得分散于各处,否则会导致类型不一致或查找失败
实现中存在的问题

- 还用之前这张图,来说明, 我们要实现 policy guard
- 通过 user 查 role 查 policy
- 再通过 permission 查 policy
- 上面两个都非常好实现
- 还有就是要把这两者之间结合起来
- 主要关注 role policy 和 permission policy 的实现
- 重点是两者的查询和更新问题
- 关联的时候,存在2个问题
- 不同类型的判断
- 查询逻辑的设计
关键实践如下 (核心代码示例)
1 )关于 role 和 policy 的关联
1.1 关于 role 的 dto 部分
// src/role/dto/create-role.dto.ts
import { CreatePermissionDto } from '@/permission/dto/create-permission.dto';
import { CreatePolicyDto } from '@/policy/dto/create-policy.dto';
import { Type, Transform, plainToInstance } from 'class-transformer';
import { IsNotEmpty, IsOptional, IsString, IsArray } from 'class-validator';interface PermissionType {id?: number;name: string;action: string;description?: string;
}export class CreateRoleDto {@IsNotEmpty()@IsString()name: string;@IsOptional()@IsString()description?: string;@IsOptional()@IsArray()// permissions 两种类型: 1.string[] 2. 对象[]// 当传递为对象 -> 直接转换成对应的 dto 实例@Type(() => CreatePermissionDto) // 这里是偷懒的写法,如果传递的和类型不匹配,会直接走到后面的@Transform(( { value }) => {return value.map((item) => {if (typeof item === 'string') {// 当传递为 string -> split -> { name, action } 对象数组const parts = item.split(":");return plainToInstance(CreatePermissionDto, {name: item,action: parts[1] || '',})}return plainToInstance(CreatePermissionDto, item);})})permissions?: PermissionType[] | string[];@IsOptional()@IsArray()@Type(() => CreatePolicyDto)policies?: any
}// src/role/dto/public-role.dto.ts
import { Transform } from 'class-transformer';
import { CreateRoleDto } from "./create-role.dto";export class PublicRoleDto extends CreateRoleDto {@Transform(( { value } ) => value.map((permission) => permission.permission.name))rolePermissions: any[]
}// src/role/dto/public-update-role.dto.ts
import { PartialType } from "@nestjs/mapped-types";
import { CreateRoleDto } from "./create-role.dto";
import { CreatePolicyDto } from "@/policy/dto/create-policy.dto";
import { Expose, Transform, Type } from "class-transformer";export class PublicUpdateRoleDto extends PartialType(CreateRoleDto) {@Type(() => CreateRoleDto)@Expose({ name: 'rolePolicy'})@Transform(( { value } ) => value.map((item) => {const { policy } = item;delete policy["encode"];return item;}))policies?: any;
}
1.2 关于 role 的 service 部分
import { Inject, Injectable } from '@nestjs/common';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { PrismaClient } from 'prisma-postgresql';
// import { PrismaClient } from '@prisma/client';
import { PRISMA_DB_CLIENT } from '@/database/database.constants';@Injectable()
export class RoleService {constructor(@Inject(PRISMA_DB_CLIENT) private prismaClient: PrismaClient) {}async create(createRoleDto: CreateRoleDto) {return await this.prismaClient.$transaction(async (prisma: PrismaClient) => {const { permissions, policies, ...restData } = createRoleDto;const rolePermissions = {create: permissions.map((permission) => ({permission: {// 没有查询到,就会创建connectOrCreate: {where: {name: permission.name,},create: {...permission,},},},})),};const rolePolicy = {create: (policies || []).map((policy) => {let whereCond:Record<string, any>;if (policy.id) {whereCond = { id: policy.id }} else {const encode = Buffer.from(JSON.stringify(policy)).toString('base64');whereCond = { encode };policy.encode = encode;}return {policy: {connectOrCreate: {where: whereCond,create: {...policy,},},},}}),}return prisma.role.create({data: {...restData,rolePermissions,rolePolicy},});},);}// ...findAllByIds(ids: number[]) {return this.prismaClient.role.findMany({where: {id: {in: ids,},},include: {rolePermissions: {include: {// 这里:联合查询是否需要变成大写,对应 schema.prisma, 这里先不管permission: true,}},rolePolicy: {include: {policy: true,}}}})}update(id: number, updateRoleDto: UpdateRoleDto) {return this.prismaClient.$transaction(async (prisma: PrismaClient) => {const { policies, ...restData } = updateRoleDto;const updateRole = await prisma.role.update({where: { id },data: {...restData,rolePolicy: {deleteMany: {}, // 删除 rolePolicy 表中历史的关联关系,继而为下面新创建做准备create: (policies || []).map((policy) => {let whereCond: Record<string, any>;if (policy.id) {whereCond = { id: policy.id };} else {const encode = Buffer.from(JSON.stringify(policy)).toString('base64');whereCond = { encode };policy.encode = encode;}return {policy: {connectOrCreate: {where: whereCond,create: {...policy,}}}}})},},include: {rolePolicy: {include: {policy: true,}}}})return updateRole;})}// ...
}
2 )关于 permission 和 policy 模块的关联
2.1 关于 permission 的 dto 部分
// src/permission/dto/create-permission.dto.ts
import { CreatePolicyDto } from '@/policy/dto/create-policy.dto';
import { Type } from 'class-transformer';
import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';// 按照 schema 来写 dto
export class CreatePermissionDto {@IsNotEmpty()@IsString()name: string;@IsString()action: string;@IsOptional()@IsString()description?: string;@IsOptional()@IsArray()@ValidateNested({each: true}) // 嵌套的进行验证@Type(() => CreatePolicyDto)policies?: any;
}// src/permission/dto/public-update-permission.dto.ts
import { PartialType } from "@nestjs/mapped-types";
import { CreatePermissionDto } from "./create-permission.dto";
import { CreatePolicyDto } from "@/policy/dto/create-policy.dto";
import { Expose, Transform, Type } from "class-transformer";export class PublicUpdatePermissionDto extends PartialType(CreatePermissionDto) {@Type(() => CreatePolicyDto)@Expose({ name: 'permissionPolicy'})@Transform(( { value } ) => value.map((item) => {const { policy } = item;delete policy["encode"];return item;}))policies?: any;
}// src/permission/dto/update-permission.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePermissionDto } from './create-permission.dto';export class UpdatePermissionDto extends PartialType(CreatePermissionDto) {}
2.2 关于 permission 的 service 部分
import { Inject, Injectable } from '@nestjs/common';
import { CreatePermissionDto } from './dto/create-permission.dto';
import { UpdatePermissionDto } from './dto/update-permission.dto';
import { PrismaClient } from 'prisma-postgresql';
import { PRISMA_DB_CLIENT } from '@/database/database.constants';@Injectable()
export class PermissionService {constructor(@Inject(PRISMA_DB_CLIENT) private prismaClient: PrismaClient) {}async create(createPermissionDto: CreatePermissionDto) {return await this.prismaClient.$transaction(async (prisma: PrismaClient) => {const { policies, ...restData} = createPermissionDto;const permissionPolicy = {create: (policies || []).map((policy) => {let whereCond;if (policy.id) {whereCond = { id: policy.id };} else {const encode = Buffer.from(JSON.stringify(policy)).toString('base64');whereCond = { encode };policy.encode = encode;}return {policy: {connectOrCreate: {where: whereCond,create: {...policy,}}}}})}return prisma.permission.create({data: {...restData,permissionPolicy},});})}// ...// 新增这个方法findByName(name: string) {return this.prismaClient.permission.findUnique({ where: { name },include: {permissionPolicy: {include: {policy: true,}}}});}update(id: number, updatePermissionDto: UpdatePermissionDto) {return this.prismaClient.$transaction(async (prisma: PrismaClient) => {const { policies, ...restData } = updatePermissionDto;const updatePermission = await prisma.permission.update({where: { id },data: {...restData,permissionPolicy: {deleteMany: {}, // 删除 permissionPolicy 表中历史的关联关系,继而为下面新创建做准备create: (policies || []).map((policy) => {let whereCond: Record<string, any>;if (policy.id) {whereCond = { id: policy.id };} else {const encode = Buffer.from(JSON.stringify(policy)).toString('base64');whereCond = { encode };policy.encode = encode;}return {policy: {connectOrCreate: {where: whereCond,create: {...policy,}}}}})},},include: {permissionPolicy: {include: {policy: true,}}}})return updatePermission;})}// ...}
3 ) 完成 policy 模块: src/policy
3.1 生成 policy 模块: $ nest g res policy --no-spec
3.2 完成 policy 的 dto 部分
// policy/dto/create-policy.dto.ts
import { IsIn, IsInt, IsOptional, IsString } from 'class-validator';
type FieldType = string | string[] | Record<string, any>;export class CreatePolicyDto {@IsInt()@IsOptional()id?: number;@IsInt()type: number;@IsString()@IsIn(['can', 'cannot'])effect: 'can' | 'cannot';@IsString()action: string;@IsString()subject: string;@IsOptional()fields?: FieldType;@IsOptional()conditions?: FieldType;@IsOptional()args?:FieldType;
}// policy/dto/update-policy.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePolicyDto } from './create-policy.dto';export class UpdatePolicyDto extends PartialType(CreatePolicyDto) {}
3.3 自行完成 policy 的 crud 接口并完成测试,此处省略 …
3.4 在 policy 模块下创建 src/policy/casl-ability.service.ts 并完成
// policy/casl-ability.service.ts
import { Injectable } from '@nestjs/common';
import { AbilityBuilder, AbilityTuple, buildMongoQueryMatcher, createMongoAbility, MatchConditions, MongoAbility, PureAbility } from '@casl/ability';
import { allParsingInstructions, allInterpreters, MongoQuery } from '@ucast/mongo2js';export interface IPolicy {type: number; // 类型标识 0-json, 1-mongo, 2-functioneffect: 'can' | 'cannot'; // 判断逻辑字段action: string; // 操作标识 crudsubject: string; // 资源标识,比如字符串或Class类fields?: string[] | string; // 字段conditions?: MatchConditions | Record<string, any> | string; // 查询条件args?: string[] | string; // 针对函数场景的参数
}type AppAbility = PureAbility<AbilityTuple, MatchConditions>;
type AbilityType = MongoAbility<AbilityTuple, MongoQuery> | AppAbility;@Injectable()
export class CaslAbilityService {async buildAbility(polices: IPolicy[], args?: any) {const abilityArr: AbilityType[] = [];let ability: AbilityType;polices.forEach(policy => {switch(policy.type) {// Jsoncase 0:ability = this.handleJsonType(policy);break;// Mongocase 1:ability = this.handleMongoType(policy);break;// Functioncase 2:ability = this.handleFunctionType(policy, args);break;default:ability = this.handleJsonType(policy);break;}abilityArr.push(ability);});return abilityArr;}determineAction(effect: string, builder: any) {return effect === 'can' ? builder.can : builder.cannot;}// 针对一般的场景// can('action', 'subject', 'fields', 'conditions')handleJsonType(policy: IPolicy): AbilityType {const { can, cannot, build } = new AbilityBuilder(createMongoAbility);const action = this.determineAction(policy.effect, { can, cannot });// 基于条件来处理action的参数const localArgs = [];if (policy.fields) {localArgs.push(policy.fields);}if (policy.conditions) {const item = typeof policy.conditions === 'object' && policy.conditions['data'] ? policy.conditions['data'] : policy.conditions;localArgs.push(item);}// 第二个参数,可能是class类的实例/*// 支持如下action(policy.action, policy.subject, conditions);action(policy.action, policy.subject, policy.fields);action(policy.action, policy.subject, policy.fields, conditions);*/action(policy.action, policy.subject, ...localArgs); // 第二个参数可以是 stringreturn build();}// 针对 Mongo 查询场景handleMongoType(policy: IPolicy):AbilityType {const { can, cannot, build } = new AbilityBuilder(createMongoAbility);const action = this.determineAction(policy.effect, { can, cannot });const conditionsMatcher = buildMongoQueryMatcher(allParsingInstructions,allInterpreters,);const conditions = (typeof policy.conditions === 'string' ? JSON.parse(policy.conditions || '{}') : policy.conditions) || {};action(policy.action, policy.subject, conditions);return build({conditionsMatcher,})}// 针对于函数的场景handleFunctionType(policy: IPolicy, args?: any):AbilityType {const { can, cannot, build } = new AbilityBuilder<AppAbility>(PureAbility);const action = this.determineAction(policy.effect, { can, cannot });const lambdaMatcher = (matchConditions: MatchConditions) => matchConditions;let func;if (policy?.args?.length && policy?.conditions['data']) {func = new Function(...policy.args, 'return ' + policy.conditions['data']);} else {func = new Function('return ' + policy.conditions);}/*if (policy?.args?.length) {let arr = [];if (typeof policy.args === 'string') {arr = policy.args.split(',');func = new Function(...arr, 'return ' + policy.conditions);} else {func = new Function(...policy.args, 'return ' + policy.conditions);}} else {func = new Function('return ' + policy.conditions);}*/action(policy.action, policy.subject, func(...args));return build({conditionsMatcher: lambdaMatcher})}
}
3.5 将 CaslAbilityService
服务暴露出来
import { Module } from '@nestjs/common';
import { PolicyService } from './policy.service';
import { PolicyController } from './policy.controller';
import { CaslAbilityService } from './casl-ability.service';@Module({controllers: [PolicyController],providers: [PolicyService, CaslAbilityService],exports: [CaslAbilityService]
})
export class PolicyModule {}
4 ) 完成 policy guard 模块
4.1 生成 policy 的 guards
- $
nest g guard common/guards/policy --no-spec --flat
4.2 创建 policy guard: src/common/guards/policy.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { CaslAbilityService, IPolicy } from '@/policy/casl-ability.service';
import { permittedFieldsOf } from '@casl/ability/extra';
import { Reflector } from '@nestjs/core';
import { UserRepository } from '@/user/user.repository';
import { RoleService } from '@/role/role.service';
import { ConfigService } from '@nestjs/config';
import { PERMISSION_KEY } from '../decorators/role-permission.decorator';
import { PermissionService } from '@/permission/permission.service';
import { SharedService } from '@/modules/shared/shared.service';
import { subject } from '@casl/ability';
import { User } from '@/user/user.entity';
import { plainToInstance } from 'class-transformer';
import { Subject } from 'rxjs';
import { mapSubjectToClass } from '@/modules/shared/shared.utils';@Injectable()
export class PolicyGuard implements CanActivate {constructor(private reflector: Reflector,private userRepository: UserRepository,private roleService: RoleService,private permissionService: PermissionService,private configService: ConfigService,private caslAbilityService: CaslAbilityService,private sharedService: SharedService) {}async canActivate(context: ExecutionContext,): Promise<boolean> {const req = context.switchToHttp().getRequest();const { username } = req.user;if (!username) return false;// 1. Guard -> 装饰器 handler & class permission nameconst classPermission = this.reflector.get(PERMISSION_KEY, context.getClass())const handlerPermission = this.reflector.get(PERMISSION_KEY, context.getHandler())const cls = classPermission instanceof Array ? classPermission.join('') : classPermission;const handler = handlerPermission instanceof Array ? handlerPermission.join('') : handlerPermission;const right = `${cls}:${handler}`;console.log('right: ', right);// 2. permission -> Policy 需要访问接口的数据权限const permissionPolicy = await this.permissionService.findByName(right);console.log('permissionPolicy: ', permissionPolicy);// 3. Policy -> subjects -> 缩小 RolePolicy 的查询范围const subjects = permissionPolicy.permissionPolicy.map((policy) => policy.policy.subject);// 4. username -> User -> Role -> Policy & subjects 用户已分配接口权限const user = await this.userRepository.findOne(username); // 这里原本不包含user的policy信息// console.log(user);// console.log('------');const roleIds = user.userRole.map(role => role.roleId);// console.log('roleIds: ', roleIds);// 判断是否是白名单const whitelist = this.configService.get('ROLE_ID_WHITELIST');if (whitelist) {const whitelistArr = whitelist.split(',');// console.log('whitelistArr: ', whitelistArr);// 判断whiltelistArr中包含roleIds中的数据,则返回 trueif (whitelistArr.some(id => roleIds.includes(+id))) return true;}const rolePolicy = await this.roleService.findAllByIds(roleIds);console.log(rolePolicy);// 得到 policy 数组const rolePolicyFilterBySubjects = rolePolicy.reduce((acc, cur) => {const rolePolicy = cur.rolePolicy.filter((policy) => {return subjects.includes(policy.policy.subject);});acc.push(...rolePolicy);return acc;}, []);// console.log('rolePolicyFilterBySubjects: ', rolePolicyFilterBySubjects);// 基于上面的 policy 来创建 ability 实例 与上面 permissionPolicy 进行比较判断const polices = rolePolicyFilterBySubjects.map(o => o.policy);if (polices.length === 0) return true; // 接口不需要任何数据权限控制user.rolePolicy = rolePolicy;user.polices = polices;user.roleIds = roleIds;user.permissions = user.userRole.reduce((acc, cur) => {return [...acc, ...cur.role.rolePermissions];}, []);delete user.password; // 删除敏感信息// console.log(user);// console.log('------------');// `const flag = ability.can('update', article, 'title');` can 后的第二个参数可以是 string 也可以是 class 实例const abilities = await this.caslAbilityService.buildAbility(polices, [ user, req, this.reflector ]);let allPermissionsGranted = true;const tmpPermissionsPolicy = [...permissionPolicy.permissionPolicy];// 第一层循环是循环接口的 policyfor (const policy of tmpPermissionsPolicy) {const { action, subject, fields } = policy.policy;let permissionGranted = false;// 第二层循环是循环 abilitiesfor (const ability of abilities) {// map -> {string -> subject: function(user)} 得到的实例传递const data = await this.sharedService.getSubject(subject, user);const subjectTmp = mapSubjectToClass(subject);const subjectObj = typeof subjectTmp === 'string' ? subjectTmp : plainToInstance(subjectTmp, data);if (!fields) {permissionGranted = ability.can(action, subjectObj);} else {let fieldHolder: Record<string, any>;if (fields instanceof Array && fields.length) fieldHolder = fields;if (fields['data']) fieldHolder = fields['data'];permissionGranted = fieldHolder.every(field => ability.can(action, subjectObj, field + ''));}if (permissionGranted) break;}if (permissionGranted) {const index = tmpPermissionsPolicy.indexOf(policy);if (index > -1) {tmpPermissionsPolicy.splice(index, 1);}}}if (tmpPermissionsPolicy.length !== 0) allPermissionsGranted = false;return allPermissionsGranted;}
}
查询逻辑的设计,从小的局部的开始查起,避免大的性能消耗
主要过程如下:
- 1 ) 用户访问 Guard -> 拿到装饰器上的 handler & class 里面的 permission 的 name
- 2 ) 通过 permission 拿到当前路由上的 policy (记录了有哪些用户可以访问, 以及访问的字段是什么)
- 这些字段帮助我们判断逻辑帮助不是很大, 实际上我们可以把它们绑定到上下文上面去
- 就像 authGuard jwt 一样修改 request 上的user属性, 这是需要访问接口的数据权限
- 3 )有了 policy 获取里面的 subjects, 缩小 rolePolicy 的查询范围
- 4 ) 根据 username 查询到 User 查询到 Role 查询到 Policy & subjects 用户已分配的接口权限
如下图所示

- 左侧是接口通过permission读取出来的 policy 有4个,
- 右侧是通过username 读取到 role, 读取到policy 形成 对应 4个 ability
- 也就是针对 4个 ability 和 4个 policy 做一个判断
- 对于 ability1 依次用 policy1~4 来进行判断, 只要有其中一个 policy 响应回来的是 true
- 就让 ability1 得到的结果为 true, 这时候就可以删除 policy1 了
- 接着对于 ability2 依次用 policy2~4 重新做判断, 使用同样的逻辑来判断
- 直到 最后 一个 ability 做判断,如果剩下的 policy 都判断完了,得到的都是 false
- 那么 最终在 guard 中得到的就是 false
何时为 true 呢?
- 所有 policy 在某个 ability 上全部为true, 相当于左侧policy数组中没有数据了,剩余长度为0
- 即便右侧还有 多个 ability,也不需要进行判断了,直接响应回去 权限为 true
- 最极限的一种场景:前面3个ability都判断完了, 都为 true,到最后一个 ability4的时候,剩下的一个 policy4 也是 true
- 这样,前面的所有 ability 都是 true,而目前的 policy 列表长度也空了,同样代表它拥有操作该接口的权限
x ) 将 policy guard 应用到 user 模块的接口上进行测试
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { Permission, Read, Update } from '@/common/decorators/role-permission.decorator';
import { RolePermissionGuard } from '@/common/guards/role-permission.guard';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PolicyGuard } from '@/common/guards/policy.guard';@Controller('user')
// @UseGuards(AuthGuard('jwt'), AdminGuard, RolePermissionGuard)
// @UseGuards(JwtGuard, AdminGuard, RolePermissionGuard)
@UseGuards(JwtGuard, RolePermissionGuard, PolicyGuard)
@Permission('user')
// @Permission('user1') // 后期再支持
export class UserController {constructor(private userRepository: UserRepository,) {}@Get()// @Read()@Update()findAll(@Query('page', new ParseIntPipe({ optional: true})) page,@Query('limit', new ParseIntPipe({optional: true})) limit,) {return this.userRepository.findAll(page, limit);}
}
总结
1 ) 尽管主干逻辑已完备,但仍存在若干需关注的扩展点:
- 白名单机制未完全展开:对于某些无需权限校验的公共接口(如登录、注册),应明确配置白名单路由,避免误拦截
- 高安全级别模块优先集成:建议优先在用户管理、财务结算、敏感数据导出等关键模块启用 PolicyGuard,逐步推进全系统覆盖
- 运行时性能考量:随着策略数量增长,双重循环可能带来性能瓶颈,未来可考虑引入索引缓存或规则归约优化
2 ) 综上所述
- 本策略权限系统通过深度融合 CASL 的能力模型、Prisma/MongoDB 的查询兼容性、TypeScript 的静态类型优势以及 NestJS 的守卫机制,构建了一套可扩展、易维护、语义清晰的细粒度权限控制体系
- 其核心价值不仅体现在功能完整性上,更在于为后续实现属性基访问控制(ABAC)、策略即代码(Policy-as-Code)等高级范式奠定了坚实基础