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

相关推荐
恋猫de小郭30 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌6 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX8 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法8 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate