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一下哦彦祖们~😄

相关推荐
永乐春秋13 分钟前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿15 分钟前
【前端】CSS
前端·css
ggdpzhk16 分钟前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
活宝小娜5 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点5 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow5 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o5 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā6 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue