Appearance
原理理解
下面我们自己实现个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装饰器进行整合