手摸手,带你实现Nestjs接口权限控制

image.png

nestjs介绍

Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。

在底层,Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。

实现

权限控制采用经典的 rbac 权限模型,通过nestjs守卫实现。程序启动的时候,定时任务会把所有的权限信息缓存到redis中,然后请求经过中间件,到达守卫的时候,守卫开始鉴权。

数据库模型

角色表

import { modelOptions, prop } from '@typegoose/typegoose'

import { ApiProperty } from '@nestjs/swagger';

import { IsInt,IsString,IsNotEmpty } from 'class-validator';//验证库。搭配nest验证管道使用更香呦!

import { ApiErrorCode } from '@app/common/filters/api-exception.filter'//自定义返回错误 由nest异常过滤器捕获 包装返回给前端使错误格式统一化



@modelOptions({

    schemaOptions:{

        timestamps:true

    }

})

export class Role{



    @IsNotEmpty({ message: '角色名称不能为空', context: { errorCode: ApiErrorCode.FORMAT_ERROR }})

    @ApiProperty({description:'角色名称',example:'Super Admin'})

    @prop()

    role_name:string



    @IsNotEmpty({ message: '角色编码不能为空', context: { errorCode: ApiErrorCode.FORMAT_ERROR }})

    @ApiProperty({description:'角色编码',example:'admin'})

    @prop()

    encoded:string



    @ApiProperty({description:'角色描述',example:'超级管理员',required:false})

    @prop()

    description?:string



    @IsInt({ message: '状态值为数字', context: { errorCode: ApiErrorCode.FORMAT_ERROR }})

    @ApiProperty({description:'状态',example:1,required:false})

    @prop({default:1,enum:[0,1]})

    status:number





}
复制代码

权限表

import { modelOptions, mongoose, prop, Ref } from '@typegoose/typegoose'

import { ApiProperty } from '@nestjs/swagger';

import { IsInt,IsString,IsNotEmpty } from 'class-validator';

import { ApiErrorCode } from '@app/common/filters/api-exception.filter'





export class VueMeta{



    

    @ApiProperty({description:'路由菜单名称',example:'dashboard'})

    @prop()

    title:string



    @ApiProperty({description:'路由菜单图标',example:'dashboard'})

    @prop()

    icon:string



}




@modelOptions({

    schemaOptions:{

        timestamps:true

    }

})

export class Access{

    @ApiProperty({description:'菜单或路径',example:'0,1'})

    @prop({enum:[0,1]})

    type:number



     //@IsNotEmpty({ message: '请求路径不能为空', context: { errorCode: ApiErrorCode.FORMAT_ERROR }})

    @ApiProperty({description:'请求路径',example:'/'})

    @prop()

    req_url:string



    @ApiProperty({description:'请求方式',example:'get'})
    @IsIn(['GET','POST','PUT','DELETE']{message:'请求方式类型错误',context: { errorCode: ApiErrorCode.FORMAT_ERROR }})
    @prop({enum:['GET','POST','PUT','DELETE']})

    req_method:string



    //@IsNotEmpty({ message: '上级不能为空', context: { errorCode: ApiErrorCode.FORMAT_ERROR }})

    @ApiProperty({description:'上级',example:'/'})

    @prop({ref:'Access',type:()=>mongoose.Schema.Types.Mixed})

    parent:Ref<Access,any>



    @ApiProperty({description:'路由路径',example:'/'})

    @prop()

    path:string



    // @ApiProperty({description:'路由源信息',example:'vue路由源信息'})

    // @prop()

    // meta:VueMeta



    @ApiProperty({description:'菜单标题',example:'菜单标题'})

    @prop() 

    meta_title:string



    @ApiProperty({description:'菜单图标',example:'菜单标题'})

    @prop()

    meta_icon:string



    @ApiProperty({description:'路由菜单是否隐藏',example:'是 否'})

    @prop({type:Boolean})

    hidden:boolean



    @ApiProperty({description:'路由跳转',example:'/'})

    @prop()

    redirect:string



    @ApiProperty({description:'路由名称',example:'name'})

    @prop()

    name:string



    @ApiProperty({description:'路由文件路径',example:'dashboard/index'})

    @prop()

    component:string



    @ApiProperty({description:'是否为动态路由',example:'是 否'})

    @prop({type:Boolean})

    props:boolean





    @ApiProperty({description:'状态',example:1,required:false})

    @prop({default:1,enum:[0,1]})

    status:number



    @ApiProperty({description:'顶级菜单排序',example:1,required:false})

    @prop({type:Number})

    order:number

}





/* @prop({ref:()=>Access,type:()=> String})

parent:Ref<Access,string> */
复制代码

角色权限表

import { modelOptions, prop, Ref } from '@typegoose/typegoose'

import { ApiProperty } from '@nestjs/swagger';

import { IsInt,IsString,IsNotEmpty } from 'class-validator';

import { ApiErrorCode } from '@app/common/filters/api-exception.filter'

import { Access } from './access.model';

import { Role } from './role.model';



@modelOptions({

    schemaOptions:{

        timestamps:true

    }

})

export class RoleAccess{



    @prop({ref:'Access'})

    access_id:Ref<Access>



        

    @prop({ref:'Role'})

    role_id:Ref<Role>

}
复制代码

守卫

import { CanActivate, ExecutionContext, Injectable, ForbiddenException, HttpException, HttpStatus } from '@nestjs/common';

import { Observable } from 'rxjs';

import { CacheService } from '@libs/db/cache/cache.service';//redis服务



const white = ['/code', '/login', '/menu', '/admin/user/:id', '/cloud/flow']//白名单





@Injectable()

export class AdminauthGuard implements CanActivate {

  constructor(

    private readonly cacheservice: CacheService,

  ) { }

  async canActivate(

    context: ExecutionContext,

  ): Promise<boolean> {

    const request = context.switchToHttp().getRequest();



    return true

    let role = request.session?.$user?.role_id //登录成功的时候会在session上写入用户的角色ID

    let path = request.route.path //请求的url 如果是restful风格的/user/:id 例如(请求路径是/user/12313 此时得到的是/user/:id)

    let method = request.method 

    if (white.indexOf(path) !== -1 || role?.encoded === 'super_admin'//超级管理员拥有所有权限) {

      return true;

    } else {

        if(!role){

          throw new HttpException('请重新登陆以获取授权', HttpStatus.FORBIDDEN);

        }
        //使用redis的hash结构存储。key用户的角色id. value为json字符串的权限数组
        let Access: any[] = await this.cacheservice.hget('RoleAccess', role._id)

        if (Access instanceof Array && Access.length > 0) {

          let hasAuth = Access.some((r) => r.req_url === path && r.req_method === method)

          if (hasAuth) {

            return true

          } else {

            throw new HttpException('暂无权限,请联系管理员', HttpStatus.FORBIDDEN);

          }

        } else {

          throw new HttpException('暂无权限,请联系管理员', HttpStatus.FORBIDDEN);

        }

    }

  }

}
复制代码

挂载守卫到全局 app.module.ts

@Module({

  imports: [

    CommonModule,

    ScheduleModule.forRoot(),

    WinstonModule.forRoot({

      transports: [logConsole, logFile],

    }),

    UsersModule,

    AdminUserModule,

    VideoModule,

    MusicModule,

    TalkModule,

    TasksModule,

    SystemSettingsModule,

    CloudModule,

    MinioModule,

    OperateModule,

    QueueModule,   

  ],

  controllers: [],

  providers: [

    {

      provide:APP_GUARD,

      useClass:AdminauthGuard//就是这个权限的守卫 写在提供者里面

    },

    {

      provide: APP_PIPE,

      useClass: ApiDtoPipe

    },

    {

      provide: APP_FILTER,

      useClass: AllExceptionsFilter

    },

    {

      provide: APP_FILTER,

      useClass: HttpExceptionFilter

    },

    {

      provide: APP_INTERCEPTOR,

      useClass: TransformInterceptor

    },

    {

      provide: APP_INTERCEPTOR,

      useClass:LogInterceptor

    }

  ],

})
复制代码

猜你喜欢

转载自juejin.im/post/7047020012627820581