casl概述
rabc模型中只实现了粗放的接口、菜单、按钮权限,数据权限无法实现。通过CASL访问列表控制可以实现更细粒度的权限控制,如作者角色有编辑文章的权限,在RABC中就可以实现该权限,但要实现只能编辑自己文章的业务规则就只能硬编码了。在CASL中编辑文章有两条控制策略:编辑自己文章、编辑所有文章的状态为已推荐,作者可以编辑自己文章,管理员可以推荐文章,通过路由守卫在用户访问文章编辑接口时,根据登录人的角色,查出角色拥有的控制列表,再查出该接口许可条件中的控制列表,两个列表进行比较就可以知道该用户能否访问该接口。 表结构设计:(只列出主要字段)
user: 用户拥有多个角色
userId:string,
userName:string,
roles:Role[]
role:角色有多个权限,角色有多个策略
roleId:string,
roleName:string,
permissions:Permission[],
policies:Policy[]
permission: 权限有多个策略
permissionId:string,
permissionName:string,
roles:Role[],
policies:Policy[]
policy:策略
policyId:string,
policyName:string,
effect:string;//允许can 拒绝cannot
action:string;//操作 增删改查+manage
subject:string;//如文章、订单
fields?:string;//如文章状态、文章作者
conditions?:string;//如editor=currentuser.id
args?:string;//args->函数的参数,如用户编号
roles:Role[],
permissions:Permission[],
数据举例:
- 角色有管理员、作者两个角色。
- 文章的update接口需要document:update权限
- 这个document:update权限有两种策略:置顶文档(实际就是更新document的一个是否置顶这个字段为是)、修改作者自己的文档。
- 管理员角色呢有两个策略,置顶文档、修改文档;作者角色只能修改文档,无权置顶文档。
判断逻辑:
- 取到接口注解的权限
- 取到注解权限对应的所有策略(指定+修改)
- 根据当前用户取到该用户所属的所有角色对应的权限,如当前登陆人为作者,则只取到修改文档策略。如果是管理员,则取到置顶+修改文档两条策略。
- 循环检查所有规则
代码实现
1、创建策略模块
1.1、创建模块
nest g res syste,/policy --no-spec
1.2、修改policy.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { Policy } from './entities/policy.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { ListPolicyDto } from './dto/list-policy.dto';
@Injectable()
export class PolicyService {
constructor(
@Inject(DataSource)
private dataSource: DataSource,
@InjectRepository(Policy)
private readonly policyRepository: Repository<Policy>,
) {}
async getUserPolicies(userId: string): Promise<Policy[]> {
let queryBuilder = this.dataSource
.getRepository(Policy)
.createQueryBuilder('p')
.leftJoinAndSelect('p.roles', 'r')
.leftJoinAndSelect('r.users', 'u');
return queryBuilder.where('u.userId = :userId', { userId }).getMany();
}
}
1.3、修改policy.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, Inject } from '@nestjs/common';
import { PolicyService } from './policy.service';
@Controller('policy')
export class PolicyController {
constructor(private readonly policyService: PolicyService,
) {}
@Get(':userid')
async getUserPolicies(@Param('userid') userid: string) {
return await this.policyService.getUserPolicies(userid);
}
}
1.4、修改policy.module.ts
import { Global, Module, forwardRef } from '@nestjs/common';
import { PolicyService } from './policy.service';
import { PolicyController } from './policy.controller';
import { UserModule } from '../user/user.module';
import { RoleModule } from '../role/role.module';
import { PermissionModule } from '../permission/permission.module';
import { SharedModule } from 'src/shared/shared.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Policy } from './entities/policy.entity';
import { AbilityFactory } from './ability.factory';
@Global()
@Module({
imports: [
forwardRef(() => UserModule),
forwardRef(() => RoleModule),
forwardRef(() => PermissionModule),
SharedModule,
TypeOrmModule.forFeature([Policy]),
],
controllers: [PolicyController],
providers: [PolicyService,AbilityFactory],
exports: [PolicyService,AbilityFactory],
})
export class PolicyModule {}
1.5、policy.entity.ts
定义一个实体类
import { ApiProperty } from "@nestjs/swagger";
import { IsOptional } from "class-validator";
import { Permission } from "src/system/permission/entities/permission.entity";
import { Role } from "src/system/role/entities/role.entity";
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity('sys_policy')
export class Policy {
@ApiProperty({
example: "自动生成",
description: "用户ID",
})
@PrimaryGeneratedColumn('uuid',{name: 'policy_id', comment: '策略ID' })
public policyId:string;
@Column({ type: 'varchar', length: 10, comment: '策略类型' })
type:string;//类型 json mongo func
@Column({ type: 'varchar', length: 50, comment: '策略effect' })
effect:string;//允许can 拒绝cannot
@Column({ type: 'varchar', length: 50, comment: '策略action' })
action:string;//操作 增删改查+manage
@Column({ type: 'varchar', length: 200, comment: '策略subject' })
subject:string;//subject->class类的实例 data->数据库中的数据
@IsOptional()
@Column({ type: 'varchar', length: 2000, nullable: true,comment: '策略fields' })
fields?:string;//fields->数据库中的字段,{type,data}
@Column({ type: 'json', nullable: true,comment: '策略条件' })
conditions?:any;// conditions->数据库中的条件
@IsOptional()
@Column({ type: 'varchar', length: 2000, nullable: true,comment: '函数参数' })
args?:string;//args->函数的参数
//角色策略
@IsOptional()
@ManyToMany(() => Role)
@JoinTable({
name: 'sys_role_policy',
joinColumn: { name: 'policy_id', referencedColumnName: 'policyId' },
inverseJoinColumn: { name: 'role_id', referencedColumnName: 'roleId' }
})
roles?: Role[];
//权限策略
@IsOptional()
@ManyToMany(() => Permission)
@JoinTable({
name: 'sys_permission_policy',
joinColumn: { name: 'policy_id', referencedColumnName: 'policyId' },
inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'permissionId' }
})
permissions?:Permission[];
}
1.6、定义注解
import { SetMetadata } from "@nestjs/common";
export const PERMISSION_KEY="permission";
export const Permission=(permission:string)=>SetMetadata(PERMISSION_KEY,permission);
1.7、定义工厂方法
定义工厂方法,创建用户所有的策略(数据库中该用户的所有策略)
//ability.factory.ts
import { Injectable } from '@nestjs/common';
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
import { PolicyService } from '../policy/policy.service'; // 策略服务
import { SubjectRegistry } from 'src/shared/subject.registry';
import { User } from '../user/entities/user.entity';
import { SharedService } from 'src/shared/shared.service';
@Injectable()
export class AbilityFactory {
constructor(private readonly policyService: PolicyService,
private sharedService: SharedService,
) {}
async createForUser(user: User,inArgs:any) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
//todo:这是测试示例1
// can('read', 'Article', { authorId: "1" });
// return build();
//todo:这是测试示例2
// const args = ['user']; // 假设从数据库获取
// const strfunc = "() => ({ authorId: user.userId })"; // 假设从数据库获取
// // 修正1:正确创建函数并绑定参数
// const func = new Function(...args, `return (${strfunc})()`);
// can('read', 'Article', func(user))
// return build();
// 从数据库获取用户的所有策略
const policies = await this.policyService.getUserPolicies(user.userId);
// 应用策略规则
for (const policy of policies) {
const subjectType = SubjectRegistry.getType(policy.subject);
if (!subjectType) {
throw new Error(`未注册的 subject: ${policy.subject}`);
}
const action=this.determineAction(policy.effect,{can,cannot});
if (policy.action === 'manage') {
// 管理所有操作
can('manage', subjectType, policy.conditions);
} else if (policy.fields?.length > 0) {
// 字段级权限
action(policy.action, subjectType, policy.fields, policy.conditions);
} else {
// 整个实体的权限
if(policy.conditions.type==='object'){
action(policy.action,policy.subject,null,JSON.parse(policy.conditions.data));
}
else{
const args = [policy.args]; // 假设从数据库获取
const strfunc = policy.conditions['data']; // 假设从数据库获取
const func = new Function(...args, `return (${strfunc})()`);
action(policy.action, policy.subject, func(...inArgs));
}
}
};
return build();
}
determineAction(effect:string,builder:any){
return effect==='can'?builder.can:builder.cannot;
}
}
1.8、定义守卫
定义守卫,判断用户所有策略和注解上的策略是否匹配
//policy.guard.ts
import { CanActivate, ExecutionContext, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AbilityFactory } from './ability.factory';
import { EntityManager } from 'typeorm';
import { PERMISSION_KEY } from './policies.decorator';
import { PermissionService } from '../permission/permission.service';
import { Policy } from './entities/policy.entity';
import { SharedService } from 'src/shared/shared.service';
import { PureAbility, Subject } from '@casl/ability';
export type AppAbility = PureAbility<[string, Subject]>;
@Injectable()
export class PoliciesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private abilityFactory: AbilityFactory,
//private entityManager: EntityManager,
@Inject(PermissionService)
private permissionService: PermissionService,
private sharedService: SharedService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 获取注解权限许可
const permission = this.reflector.get<string>(PERMISSION_KEY, context.getHandler());
if (!permission) return true;//没有则表示不管控权限
//根据权限许可拿到权限策略
const permissionPolicy=await this.permissionService.findByName(permission);
const rules=permissionPolicy.policies;//许可的策略
// 获取用户权限
const request = context.switchToHttp().getRequest();
const user = request.user;
if(!user){
return false;
}
const ability = await this.abilityFactory.createForUser(user,[
user,
request,
this.reflector,
]);
// 检查所有规则
for (const rule of rules) {
const subject = await this.sharedService.resolveSubject(rule.subject,request);
if (!this.isAllowed(ability, rule, subject)) {
return false;
}
}
return true;
}
private isAllowed(
ability: AppAbility,
rule: Policy,
subject: any
): boolean {
// 处理字段级权限
if (rule.fields?.length > 0) {
return rule.fields.split(",").every(field =>
ability.can(rule.action, subject, field)
);
}
return ability.can(rule.action, subject);
}
}
1.9、在共享模块中定义subject实例获取方法
判断字段级别的规则,如只能更新作者自己的文章,此时必须使用subject(即article)的实例才行,所以必须能取到实例对象。
//shared.module.ts
import { Global, Module, forwardRef } from '@nestjs/common';
import { SharedService } from './shared.service';
import { SharedController } from './shared.controller';
import { SubjectRegistry } from './subject.registry';
@Global()//必须设置为全局模块
@Module({
controllers: [SharedController],
providers: [SharedService,SubjectRegistry],
exports: [SharedService,SubjectRegistry],
})
export class SharedModule {}
//shared.service.ts
import { Injectable, NotFoundException, Type } from '@nestjs/common';
import { Policy } from 'src/system/policy/entities/policy.entity';
import { EntityManager } from 'typeorm';
import { SubjectRegistry } from './subject.registry';
@Injectable()
export class SharedService {
constructor(
private entityManager: EntityManager,
){}
//在这定义一个解析对象实例的方法,在守卫中用于获取对象实例
async resolveSubject(subject:string,request: any): Promise<any> {
// 1. 如果规则指定了实例获取函数
// if (typeof rule.getSubject === 'function') {
// return rule.getSubject(request);
// }
// 2. 尝试从请求中获取已加载的实例
if (request.resourceInstance) {
return request.resourceInstance;
}
// 3. 尝试从路径参数中获取ID并加载实体
const { params } = request;
if (params?.id && subject) {
const subjectType = SubjectRegistry.getType(subject);
if (!subjectType) {
throw new Error(`未注册的 subject: ${subject}`);
}
let where;//TODO:全部主键改为id就不用这样分支判断了
if(subject.toLowerCase() === 'user'){
where={userId: params.id}
}
else{
where={id: params.id}
}
const instance = await this.entityManager.findOne(subjectType, {
where:where
});
if (!instance) {
throw new NotFoundException('资源不存在');
}
// 缓存到请求中供后续使用
request.resourceInstance = instance;
return instance;
}
// 4. 返回类作为回退
return SubjectRegistry.getType(subject);
}
}
//subject.registry.ts
//此文件定义实体类注册集合
import { Type } from '@nestjs/common';
export class SubjectRegistry {
private static readonly registry = new Map<string, Type<any>>();
static register(name: string, type: Type<any>) {
this.registry.set(name, type);
}
static getType(name: string): Type<any> | undefined {
return this.registry.get(name);
}
static getAllSubjects(): string[] {
return Array.from(this.registry.keys());
}
}
// 装饰器自动注册实体
export function RegisterSubject(name: string) {
return (target: Type<any>) => {
SubjectRegistry.register(name, target);
};
}
//shared.controller.ts
//暂时没有其他功能,可以为空
1.10、测试
新创建一个article模块用于测试:
//article.entity.ts
import { RegisterSubject } from "src/shared/subject.registry";
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@RegisterSubject('Article') //此处注册实体类
@Entity("article")
export class Article {
@PrimaryGeneratedColumn('uuid',{name: 'id', comment: 'ID' })
public id: string;
@Column({ type: 'varchar', name: 'title',nullable: true, length: 500, comment: '标题' })
title: string;
@Column({ type: 'boolean', nullable: true,name: 'is_moderator', comment: '是否版主' })
isModerator:boolean;
@Column({ type: 'varchar', nullable: true,name: 'author_id', comment: '作者' })
authorId:string;
@Column({ type: 'varchar', nullable: true,name: 'status', comment: '状态' })
status:string;
}
//article.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { Article } from './entities/article.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class ArticleService {
constructor(
@InjectRepository(Article)
protected repository: Repository<Article>,
){}
create(createArticleDto: CreateArticleDto) {
return 'This action adds a new article';
}
async findAll():Promise<Article[]> {
return this.repository.find();
}
findOne(id: string) {
return this.repository.findOne({where: {id: id}});
}
update(id: number, updateArticleDto: UpdateArticleDto) {
return `This action updates a #${id} article`;
}
remove(id: number) {
return `This action removes a #${id} article`;
}
}
//article.module.ts
import { Module } from '@nestjs/common';
import { ArticleService } from './article.service';
import { ArticleController } from './article.controller';
import { SharedModule } from 'src/shared/shared.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from './entities/article.entity';
@Module({
imports: [SharedModule,
TypeOrmModule.forFeature([Article]),
],
controllers: [ArticleController],
providers: [
ArticleService,
],
})
export class ArticleModule {
}
//article.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, Inject } from '@nestjs/common';
import { ArticleService } from './article.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { Permission } from 'src/system/policy/policies.decorator';
import { AllowNoToken } from 'src/system/auth/decorators/token.decorator';
import { SharedService } from 'src/shared/shared.service';
@Controller('article')
export class ArticleController {
constructor(private readonly articleService: ArticleService,
@Inject(SharedService) private sharedService: SharedService
) {}
@Post()
create(@Body() createArticleDto: CreateArticleDto) {
return this.articleService.create(createArticleDto);
}
@Get()
@Permission('article:read')
async findAll() {
return await this.articleService.findAll();
}
@Get(':id')
@Permission('article:read')
findOne(@Param('id') id: string) {
return this.articleService.findOne(id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateArticleDto: UpdateArticleDto) {
return this.articleService.update(+id, updateArticleDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.articleService.remove(+id);
}
}
** 测试步骤:**
表数据: sys_permission:
sys_permission_policy:
sys_policy:
sys_user:
sys_role:
sys_user_role:
sys_role_policy:
article :
第一步:
调用登录窗口取到token
第二步:
读取作者为当前登录用户的文章,正常读取:
读取第二条(参见article表数据,第二条作者id为22,不是当前登陆人)
第三步:测试使用object类型策略
把角色策略换成id=6的记录: