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
}
],
})
复制代码