Vue3+NestJS实现权限管理系统(六):接口按钮权限控制

上一篇文章我们已经知道了什么是 RBAC 权限管理并且完成了菜单相关权限的处理。当然,一个管理系统仅仅对菜单做权限管理很显然是不够的,除此之外我们在前端还需要对一些按钮进行权限管理,而对应的后端则是对这些按钮调用的接口进行权限验证。前端通过登录获取该用户所有权限字段,然后在按钮中通过自定义指令的方式传入这个按钮所需要的权限字段,通过判断这个用户的权限是否包含这个按钮的权限来控制按钮的显示与隐藏,示例如下

html 复制代码
<el-button
  type="primary"
  plain
  icon="Plus"
  @click="handleAdd"
  v-hasPermi="['system:user:add']"
  >新增</el-button
>

对于后端同样的根据登录用户获取其全部权限,在controller中定义一个自定义装饰器传入这个接口所需要的权限,然后写一个导航守卫guard拿到这个权限判断这个权限是否在这个用户的权限中决定放不放行,示例如下

js 复制代码
  @UseGuards(PermissionsGuard)
  @Permissions('system:user:add')
  @Post('/test')
  async test(@Request() req) {
    return 'success';
  }

本篇文章我们将用 NestJS 中的守卫Guard、自定义装饰器decorator结合Redis来实现后端部分

对于按钮来说,我们也将其视为一个菜单,所以添加按钮权限只需要插入一条类型为 3(按钮)的菜单和一个自定义权限字符串permission即可

自定义装饰器

src/common/decorator目录下新建permissions.decorator.ts文件,写一个自定义装饰器设置一些元数据

js 复制代码
import { SetMetadata } from "@nestjs/common";

export const Permissions = (...permissions: string[]) =>
  SetMetadata("permissions", permissions);

这样当我们使用这个装饰器如@Permissions('system:user:add'),就可以在导航守卫中获取到这个装饰器的元数据从而做一些权限判断

导航守卫

src/common/guard目录下新建permissions.guard.ts文件,写一个导航守卫

js 复制代码
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Inject,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { CacheService } from 'src/cache/cache.service';
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(
    private reflector: Reflector,

    private cacheService: CacheService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    //获取自定义装饰器配置的元数据`permissions`
    const requiredPermissions = this.reflector.get<string[]>(
      'permissions',
      context.getHandler(),
    );

    // 如果没有配置则不需要验证权限的路由,直接放行
    if (!requiredPermissions) {
      return true;
    }
    // 从request拿到当前用户登录信息(全局守卫UserGuard已经将用户信息挂载到request上)
    const request = context.switchToHttp().getRequest();
    const { user } = request;
    // 从缓存中获取用户的权限列表
    const userPermissions = await this.cacheService.get(
      `${user.sub}_permissions`,
    );
    // 用户未登录或未设置权限,拒绝访问
    if (!user || !userPermissions) {
      return false;
    }
    //判断用户是否拥有所需的权限
    const hasPermission = requiredPermissions.every((permission) =>
      userPermissions.includes(permission),
    );
    return hasPermission;
  }
}

这里用到了cacheService,在守卫中使用其它模块的service我们需要在每个用到这个守卫的地方都导入一下他的模块(CacheModule),这样显得很麻烦。所以我们可以将Redis模块改成全局模块,这样就用再导入了

js 复制代码
//src/cache/cache.module.ts
import { Global, Module } from "@nestjs/common";
import { CacheService } from "./cache.service";
import { createClient } from "redis";
import { ConfigService } from "@nestjs/config";
// 使用 @Global() 装饰器将模块设置为全局模块
@Global()
@Module({
  providers: [
    CacheService,
    {
      provide: "REDIS_CLIENT",
      async useFactory(configService: ConfigService) {
        const client = createClient({
          socket: {
            host: configService.get("RD_HOST"),
            port: configService.get("RD_PORT"),
          },
        });
        await client.connect();
        return client;
      },
      inject: [ConfigService],
    },
  ],
  exports: [CacheService],
})
export class CacheModule {}

其中用户的权限列表userPermissions是在 Redis 中存储的,所以我们需要修改一下获取路由的接口,将其存储当前用户权限,并且把接口调整为获取用户信息的接口返回给前端路由和权限列表

获取用户信息接口

调整接口名为getInfo,同时返回权限列表和路由列表

js 复制代码
  async getInfo(req): Promise<GetInfoVo> {
    //user.guard中注入的解析后的JWTtoken的user
    const { user } = req;
    //根据关联关系通过user查询user下的菜单和角色
    const userList: User = await this.userRepository
      .createQueryBuilder('fs_user')
      .leftJoinAndSelect('fs_user.roles', 'fs_role')
      .leftJoinAndSelect('fs_role.menus', 'fs_menu')
      .where({ id: user.sub })
      .orderBy('fs_menu.order_num', 'ASC')
      .getOne();

    //是否为超级管理员,是的话查询所有菜单和权限
    const isAdmin = userList.roles?.find((item) => item.role_name === 'admin');
    let routers: Menu[] = [];
    let permissions: string[] = [];
    if (isAdmin) {
      routers = await this.menuRepository.find({
        order: {
          order_num: 'ASC',
        },
        where: {
          status: 1,
        },
      });
      //获取菜单所拥有的权限
      permissions = filterPermissions(routers);
      //存储当前用户的权限
      await this.cacheService.set(`${user.sub}_permissions`, permissions, null);
      return {
        routers: convertToTree(routers),
        permissions: permissions,
      };
    }
    interface MenuMap {
      [key: string]: Menu;
    }
    // console.log(userList.roles[0].menus);

    //根据id去重
    const menus: MenuMap = userList?.roles.reduce(
      (mergedMenus: MenuMap, role: Role) => {
        role.menus.forEach((menu: Menu) => {
          mergedMenus[menu.id] = menu;
        });
        return mergedMenus;
      },
      {},
    );

    routers = Object.values(menus);
    permissions = filterPermissions(routers);
    await this.cacheService.set(`${user.sub}_permissions`, permissions, 7200);

    return {
      routers: convertToTree(routers),
      permissions,
    };
  }

其中filterPermissions函数是处理权限列表的函数,将菜单列表的 permissions 字段提取到数组中并去重

js 复制代码
//utils/filterPermissions.ts
import { Menu } from "src/menu/entities/menu.entity";

export const filterPermissions = (routers: Menu[]): string[] => {
  return [
    ...new Set(
      routers
        .map((router) => router.permission)
        .filter((permission) => permission != null)
    ),
  ];
};

最后修改一下menu.controller.ts中的接口名

js 复制代码
 @Post('/getInfo')
  @ApiOperation({ summary: '获取路由' })
  async getInfo(@Request() req) {
    return await this.menuService.getInfo(req);
  }

ok,到这里我们就完成了后端部分接口权限的验证,我们来试一下,先手动给这些菜单插入几个权限字段

然后使用user1用户登录获取到它的 token 调用menu/getInfo接口就拿到了他拥有的菜单权限及权限字段

menu.controller.ts写一个测试接口看一下我们的权限验证是否准确,先试一下sys:menu:list

js 复制代码
  @UseGuards(PermissionsGuard)
  @Permissions('sys:menu:list')
  @Post('/test')
  async test(@Request() req) {
    return 'success';
  }

调用接口看一下结果是通过的,因为 user1 用户是有这个权限的(注意 token 和上线接口一致)

再改成sys:user:list试一下(user1 用户没有这个权限)

js 复制代码
  @UseGuards(PermissionsGuard)
  @Permissions('sys:user:list')
  @Post('/test')
  async test(@Request() req) {
    return 'success';
  }

可以看到返回 403,是没有权限的,符合我们的预期

源码地址

fs_admin 记得star一下哦彦祖们~😄

相关推荐
knoci22 分钟前
【Go】-基于Gin框架的博客项目
后端·学习·golang·gin
每天都要喝奶茶23 分钟前
vue3uniapp实现自定义拱形底部导航栏,解决首次闪烁问题
前端·vue.js·uni-app
May_Xu_25 分钟前
vue3+less使用主题定制(多主题定制)可切换主题
前端·javascript·vue.js·vue·less·css3
qq_4275060825 分钟前
less解决function中return写法在浏览器被识别成Object导致样式失败的问题
前端·css·less
Elastic 中国社区官方博客31 分钟前
将你的 Kibana Dev Console 请求导出到 Python 和 JavaScript 代码
大数据·开发语言·前端·javascript·python·elasticsearch·ecmascript
北京_宏哥1 小时前
《最新出炉》系列入门篇-Python+Playwright自动化测试-41-录制视频
前端·python·测试
小霖家的混江龙1 小时前
Vite 打包 H5 如何注入版本号
前端·vite
叶不休2 小时前
DOM---鼠标事件类型(移入移出)
开发语言·前端·javascript·css·chrome·前端框架·html
爱编程的鱼2 小时前
web前后端交互方式有哪些?
前端·okhttp
Xiaoyu Wang2 小时前
Go语言八股(Ⅲ)
开发语言·后端·golang