Skip to content

原理理解

向军大叔每晚八点在 抖音bilibli 直播

xj-small

下面我们自己实现个casl验证逻辑,来理解一下策略验证。然后你在理解 Nest的casl模块就比较容易了。

casl

首先实现 casl 验证机制

import { User } from '@prisma/client'
//保存策略规则
const rules = [] as { action: string; type: any; condition: any }[]

//策略验证工具
const casl = {
  //定义策略
  can(action: string, type: string, condition: Record<string, any>) {
    rules.push({ action, type, condition })
  },
  ability: {
  	//用于控制器进行验证
    can(action: string, type: string, subject: any) {
      return rules.every((rule) => {
        const [key, value] = Object.entries(rule.condition)[0]
        return rule.action == action && rule.type == type && subject[key] == value
      })
    },
  },
}

//定义策略工厂函数
export const testFactory = (user: User) => {
  const { can, ability } = casl
  //策略定义,topic模型只允许作者更新
  can('update', 'topic', { userId: user.id })

	//返回给控制器进行验证的方法
  return { ability }
}

控制器

下面在控制器中使用策略验证

下面代码会使用的JWT等知识,如果不清楚请查看后盾人nestjs相关章节

import { Auth } from '@/auth/decorator/auth.decorator'
import { User } from '@/auth/decorator/user.decorator'
import { Body, Controller, HttpException, HttpStatus, Param, Patch } from '@nestjs/common'
import { PrismaClient, User as UserModel } from '@prisma/client'
import { testFactory } from './../casl/test'
import { UpdateTopicDto } from './dto/update-topic.dto'
import { TopicService } from './topic.service'

@Controller('topic')
export class TopicController {
  constructor(private readonly topicService: TopicService) {}

  @Patch(':id')
  @Auth()
  async update(@Param('id') id: number, @Body() updateTopicDto: UpdateTopicDto, @User() user: UserModel) {
    const topic = await new PrismaClient().topic.findUnique({ where: { id } })
    //策略验证
    const { ability } = testFactory(user)
    const state = ability.can('update', 'topic', topic)
    if (!state) {
      throw new HttpException('没有操作权限', HttpStatus.FORBIDDEN)
    }
    return this.topicService.update(+id, updateTopicDto)
  }
}

CASL

CASL 是一个授权库,它限制允许给定客户端访问的资源。比如设置管理员可以管理任何资源,普通用户只能管理自己的文章。

我们会使用到 CASL Prisma 扩展包,首先安装扩展包

pnpm add @casl/prisma @casl/ability

声明模块

下面在项目中创建模块与授权定义类

nest g module casl
nest g class casl/casl-ability.factory

然后在casl模块中定义提供者

import { CaslAbilityFactory } from './casl-ability.factory'
import { Global, Module } from '@nestjs/common'

@Global()
@Module({
  providers: [CaslAbilityFactory],
  exports: [CaslAbilityFactory],
})
export class CaslModule {}

策略工厂

下面创建文件 casl-ability.factory.ts 定义策略规则

import { AbilityBuilder, PureAbility } from '@casl/ability'
import { PrismaQuery, Subjects, createPrismaAbility } from '@casl/prisma'
import { Injectable } from '@nestjs/common'
import { Comment, User } from '@prisma/client'

//验证实体定义
export type AppAbility = PureAbility<[string, Subjects<{ User: User; Comment: Comment }>], PrismaQuery>

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: User) {
    const { can, build } = new AbilityBuilder<AppAbility>(createPrismaAbility)
    //添加验证规则
    can('delete', 'Comment', { userId: user.id })

    return build()
  }
}

控制器

下面在控制器中使用 casl 对访问进行验证

import { Auth } from '@/auth/decorator/auth.decorator'
import { User } from '@/auth/decorator/user.decorator'
import { subject } from '@casl/ability'
import { Body, Controller, Param, Patch } from '@nestjs/common'
import { User as UserModel } from '@prisma/client'
import { CaslAbilityFactory } from './../casl/casl-ability.factory'
import { UpdateTopicDto } from './dto/update-topic.dto'
import { TopicService } from './topic.service'

@Controller('topic')
export class TopicController {
  constructor(private readonly topicService: TopicService, private casl: CaslAbilityFactory) {}

  @Patch(':id')
  @Auth()
  async remove(@Param('id') id: number, @CurrentUser() user: User) {
    const comment = await this.prisma.comment.findUnique({ where: { id: +id } })
		
    const ablity = this.casl.createForUser(user)
    //使用CaslAbilityFactory中定义的验证规则,执行权限验证操作
    const state = ablity.can('delete', subject('Comment', comment))
		//验证失败时抛出异常
    if (!state) throw new ForbiddenException()
    return this.commentService.remove(+id)
  }
}

Policy 策略守卫

通过策略守卫简化 CASL 验证过程。

策略工厂

首先在策略工厂中定义验证规则,内容与上面讲解的 CASL 的授权策略类似。

import { AbilityBuilder, PureAbility } from '@casl/ability'
import { PrismaQuery, Subjects, createPrismaAbility } from '@casl/prisma'
import { Injectable } from '@nestjs/common'
import { Comment, User } from '@prisma/client'

//验证实体定义
export type AppAbility = PureAbility<[string, Subjects<{ User: User; Comment: Comment }>], PrismaQuery>

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: User) {
    const { can, build } = new AbilityBuilder<AppAbility>(createPrismaAbility)
    //添加验证规则
    can('delete', 'Comment', { userId: user.id })

    return build()
  }
}

守卫

下面定义可以在控制器中使用的Policy守卫文件 casl/policies.guard.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { User } from '@prisma/client'
import { Request } from 'express'
import { Observable } from 'rxjs'
import { PrismaService } from 'src/prisma/prisma.service'
import { AppAbility, CaslAbilityFactory } from './casl.factory'
import { CHECK_POLICY_KEY, PolicyHandle, modelType } from './check-policies.decorator'

@Injectable()
export class CaslGuard implements CanActivate {
  constructor(private reflector: Reflector, private readonly prisma: PrismaService) {}
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
  	//获取控制器传递的元数据
    const { model, handles } = this.reflector.get<{ model: modelType; handles: PolicyHandle[] }>(
      CHECK_POLICY_KEY,
      context.getHandler(),
    )
		//获取请求对象
    const request = context.switchToHttp().getRequest()
    //获取当前用户
    const user = request.user as User | undefined
    //进行权限验证,使用Promise.all处理异步
    const ability = new CaslAbilityFactory().createForUser(user)
    return Promise.all(handles.map((handle) => this.execute(handle, ability, model, user, request))).then((res) =>
      res.every((r) => r),
    )
  }
	
	//调用验证器进行验证
  async execute(handle: PolicyHandle, ability: AppAbility, model: modelType, user: User, req: Request) {
    //如果有id参数时查询模型
    const id = req.params.id
    let instance: any
    if (id) {
      instance = await this.prisma[model].findUnique({ where: { id: +req.params.id } })
    }
    //函数验证处理器
    if (typeof handle == 'function') {
      return handle(instance, req.user as User, ability)
    }
    //对象验证处理器
    return handle.handle(instance, req.user as User, ability)
  }
}

控制器

下面在控制器中通过装饰器快速进行验证

函数验证器

在装饰器传递函数进行验证

@Delete(':id')
@UseGuards(CaslGuard)
@CheckPolicies(modelType.Comment, (model: any, user, ability: AppAbility) => {
  return ability.can('delete', subject('Comment', model))
})
@Auth()
async remove(@Param('id') id: number, @CurrentUser() user: User) {
	return this.commentService.remove(+id)
}

类验证器

首先定义类验证器

import { Comment, User } from '@prisma/client'
import { AppAbility } from 'src/casl/casl.factory'
import { IPolicyHandle } from './../casl/check-policies.decorator'
export class TopicPolicy implements IPolicyHandle {
  handle(model: any, user: User, ability: AppAbility) {
    return false
  }
}

然后在控制器中使用

@Delete(':id')
@UseGuards(CaslGuard)
@CheckPolicies(modelType.Comment, new TopicPolicy())
@Auth()
async remove(@Param('id') id: number, @CurrentUser() user: User) {

  return this.commentService.remove(+id)
}

你也可以定义个聚合装饰器,将 @UseGuards(CaslGuard)与@CheckPolicies装饰器进行整合