返回介绍

角色权限

发布于 2024-01-18 22:07:39 字数 12110 浏览 0 评论 0 收藏 0

RBAC

    • RBAC 是基于角色的权限访问控制(Role-Based Access Control)一种数据库设计思想,根据设计数据库设计方案,完成项目的权限管理
    • 在 RBAC 中,有 3 个基础组成部分,分别是: 用户角色权限 ,权限与角色相关联,用户通过成为适当角色而得到这些角色的权限
  • 权限:具备操作某个事务的能力
  • 角色:一系列权限的集合

如:一般的管理系统中: 销售人员:仅仅可以查看商品信息 运营人员:可以查看,修改商品信息 管理人员:可以查看,修改,删除,以及修改员工权限等等 管理人员只要为每个员工账号分配对应的角色,登陆操作时就只能执行对应的权限或看到对应的页面

权限类型

  • 展示(菜单),如:显示用户列表,显示删除按钮等等…
  • 操作(功能),如:增删改查,上传下载,发布公告,发起活动等等…

数据库设计

数据库设计:可简单,可复杂,几个人使用的系统和几千人使用的系统是不一样的 小型项目:用户表,权限表 中型项目:用户表,角色表,权限表 大型项目:用户表,用户分组表,角色表,权限表,菜单表…

没有角色的设计

只有用户表,菜单表,两者是多对多关系,有一个关联表

缺点:

  • 新建一个用户时,在用户表中添加一条数据
  • 新建一个用户时,在关联表中添加 N 条数据
  • 每次新建一个用户需要添加 1+N(关联几个) 条数据
  • 如果有 100 个用户,每个用户 100 个权限,那需要添加 10000 条数据

基于 RBAC 的设计

用户表和角色表的关系设计:

如果你希望一个用户可以有多个角色,如:一个人即是销售总监,也是人事管理,就设计多对多关系 如果你希望一个用户只能有一个角色,就设计一对多,多对一关系

角色表和权限表的关系设计:

一个角色可以拥有多个权限,一个权限被多个角色使用,设计多对多关系

多对多关系设计

用户表与角色表是多对多关系,角色表与菜单表是多对多关系

更加复杂的设计

实现流程

  1. 数据表设计
  2. 实现角色的增删改查
  3. 实现用户的增删改查,增加和修改用户的时候需要选择角色
  4. 实现权限的增删改查
  5. 实现角色与授权的关联
  6. 判断当前登录的用户是否有访问菜单的权限
  7. 根据当前登录账户的角色信息动态显示左侧菜单(前端)

代码实现

这里将实现一个用户,部门,角色,权限的例子: 用户通过成为部门的一员,则拥有部门普通角色的权限,还可以单独给用户设置角色,通过角色,获取权限。 权限模块包括,模块,菜单,操作,通过 type 区分类型,这里就不再拆分。

关系总览:

  • 用户 - 部门:一对多关系,这里设计用户只能加入一个部门,如果设计可以加入多个部门,设计为多对多关系
  • 用户 - 角色:多对多关系,可以给用户设置多个角色
  • 角色 - 部门:多对多关系,一个部门多个角色
  • 角色 - 权限:多对多关系,一个角色拥有多个权限,一个权限被多个角色使用

数据库实体设计

用户

import {
  Column,
  Entity,
  ManyToMany,
  ManyToOne,
  JoinColumn,
  JoinTable,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { RoleEntity } from '../../role/entities/role.entity';
import { DepartmentEntity } from '../../department/entities/department.entity';

@Entity({ name: 'user' })
export class UsersEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    type: 'varchar',
    length: 30,
    nullable: false,
    unique: true,
  })
  username: string;

  @Column({
    type: 'varchar',
    name: 'password',
    length: 100,
    nullable: false,
    select: false,
    comment: '密码',
  })
  password: string;

  @ManyToMany(() => RoleEntity, (role) => role.users)
  @JoinTable({ name: 'user_role' })
  roles: RoleEntity[];

  @ManyToOne(() => DepartmentEntity, (department) => department.users)
  @JoinColumn({ name: 'department_id' })
  department: DepartmentEntity;
}

角色

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToMany,
  JoinTable,
} from 'typeorm';
import { UsersEntity } from '../../user/entities/user.entity';
import { DepartmentEntity } from '../../department/entities/department.entity';
import { AccessEntity } from '../../access/entities/access.entity';

@Entity({ name: 'role' })
export class RoleEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 30 })
  rolename: string;

  @ManyToMany(() => UsersEntity, (user) => user.roles)
  users: UsersEntity[];

  @ManyToMany(() => DepartmentEntity, (department) => department.roles)
  department: DepartmentEntity[];

  @ManyToMany(() => AccessEntity, (access) => access.roles)
  @JoinTable({ name: 'role_access' })
  access: AccessEntity[];
}

部门

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToMany,
  OneToMany,
  JoinTable,
} from 'typeorm';
import { UsersEntity } from '../../user/entities/user.entity';
import { RoleEntity } from '../../role/entities/role.entity';

@Entity({ name: 'department' })
export class DepartmentEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 30 })
  departmentname: string;

  @OneToMany(() => UsersEntity, (user) => user.department)
  users: UsersEntity[];

  @ManyToMany(() => RoleEntity, (role) => role.department)
  @JoinTable({ name: 'department_role' })
  roles: RoleEntity[];
}

权限

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  Tree,
  TreeChildren,
  TreeParent,
  ManyToMany,
} from 'typeorm';
import { RoleEntity } from '../../role/entities/role.entity';

@Entity({ name: 'access' })
@Tree('closure-table')
export class AccessEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 30, comment: '模块' })
  module_name: string;

  @Column({ type: 'varchar', length: 30, nullable: true, comment: '操作' })
  action_name: string;

  @Column({ type: 'tinyint', comment: '类型:1:模块,2:菜单,3:操作' })
  type: number;

  @Column({ type: 'text', nullable: true, comment: '操作地址' })
  url: string;

  @TreeParent()
  parentCategory: AccessEntity;

  @TreeChildren()
  childCategorys: AccessEntity[];

  @ManyToMany(() => RoleEntity, (role) => role.access)
  roles: RoleEntity[];
}

接口实现

由于要实现很多接口,这里只说明一部分,其实都是数据库的操作,所有接口如下:

根据用户的 id 获取信息:id,用户名,部门名,角色,这些信息在做用户登陆时传递到 token 中。

这里设计的是:创建用户时,添加部门,就会成为部门的普通角色,也可单独设置角色,但不是每个用户都有单独的角色。

async getUserinfoByUid(uid: number) {
获取用户
    const user = await this.usersRepository.findOne(
      { id: uid },
      { relations: ['roles'] },
    );
    if (!user) ToolsService.fail('用户 ID 不存在');

    const sql = `
    select 
    user.id as user_id, user.username, user.department_id, department.departmentname, role.id as role_id, rolename
    from
    user, department, role, department_role as dr
    where 
    user.department_id = department.id
    and department.id = dr.departmentId
    and role.id = dr.roleId
    and user.id = ${uid}`;
    
    const result = await this.usersRepository.query(sql);
    const userinfo = result[0];
    
    const userObj = {
      user_id: userinfo.user_id,
      username: userinfo.username,
      department_id: userinfo.department_id,
      departmentname: userinfo.departmentname,
      roles: [{ id: userinfo.role_id, rolename: userinfo.rolename }],
    };

// 如果用户的角色 roles 有值,证明单独设置了角色,所以需要拼接起来
    if (user.roles.length > 0) {
      const _user = JSON.parse(JSON.stringify(user));
      userObj.roles = [...userObj.roles, ..._user.roles];
    }
    return userObj;
}

// 接口请求结果:
{
    "status": 200,
    "message": "请求成功",
    "data": {
        "user_id": 1,
        "username": "admin",
        "department_id": 1,
        "departmentname": "销售部",
        "roles": [
            {
                "id": 1,
                "rolename": "销售部员工"
            },
            {
                "id": 5,
                "rolename": "admin"
            }
        ]
    }
}

结合 possport + jwt 做用户登陆授权验证

在验证账户密码通过后,possport 返回用户,然后根据用户 id 获取用户信息,存储 token,用于路由守卫,还可以使用 redis 存储,以作他用。

async login(user: any): Promise<any> {
    const { id } = user;
    const userResult = await this.userService.getUserinfoByUid(id);
    const access_token = this.jwtService.sign(userResult);
    await this.redisService.set(`user-token-${id}`, access_token, 60 * 60 * 24);
    return { access_token };
}

{
    "status": 200,
    "message": "请求成功",
    "data": {
        "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZGVwYXJ0bWVudF9pZCI6MSwiZGVwYXJ0bWVudG5hbWUiOiLplIDllK7pg6giLCJyb2xlcyI6W3siaWQiOjEsInJvbGVuYW1lIjoi6ZSA5ZSu6YOo5ZGY5belIn0seyJpZCI6NSwicm9sZW5hbWUiOiJhZG1pbiJ9XSwiaWF0IjoxNjIxNjA1Nzg5LCJleHAiOjE2MjE2OTIxODl9.VIp0MdzSPM13eq1Bn8bB9Iu_SLKy4yoMU2N4uwgWDls"
    }
}

后端的权限访问

使用守卫,装饰器,结合 token,验证访问权限

逻辑:

  • 第一步:在 controller 使用自定义守卫装饰接口路径,在请求该接口路径时,全部进入守卫逻辑
  • 第二步:使用自定义装饰器装饰特定接口,传递角色,自定义守卫会使用反射器获取该值,以判断该用户是否有权限

如下: findOne 接口使用了自定义装饰器装饰接口,意思是只能 admin 来访问

import {
  Controller,
  Get,
  Body,
  Patch,
  Post,
  Param,
  Delete,
  UseGuards,
  ParseIntPipe,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { AuthGuard } from '../../common/guard/auth.guard';
import { Roles } from '../../common/decorator/role.decorator';

@UseGuards(AuthGuard)   // 自定义守卫
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) { }

  @Get()
  async findAll() {
    const [data, count] = await this.userService.findAll();
    return { count, data };
  }

  @Get(':id')
  @Roles('admin')  // 自定义装饰器
  async findOne(@Param('id', new ParseIntPipe()) id: number) {
    return await this.userService.findOne(id);
  }
}

装饰器

import { SetMetadata } from '@nestjs/common';

// SetMetadata 作用:将获取到的值,设置到元数据中,然后守卫通过反射器才能获取到值
export const Roles = (...args: string[]) => SetMetadata('roles', args);

自定义守卫

返回 true 则有访问权限,返回 false 则直接报 403

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core'; // 反射器,作用与自定义装饰器桥接
import { ToolsService } from '../../utils/tools.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly jwtService: JwtService,
  ) { }

  // 白名单数组
  private whiteUrlList: string[] = [];

  // 验证该次请求是否为白名单内的路由
  private isWhiteUrl(urlList: string[], url: string): boolean {
    if (urlList.includes(url)) {
      return true;
    }
    return false;
  }

  canActivate(context: ExecutionContext): boolean {
    // 获取请求对象
    const request = context.switchToHttp().getRequest();

    // 验证是否是白名单内的路由
    if (this.isWhiteUrl(this.whiteUrlList, request.url)) return true;

    // 获取请求头中的 token 字段,解析获取存储在 token 的用户信息
    const token = context.switchToRpc().getData().headers.token;
    const user: any = this.jwtService.decode(token);
    if (!user) ToolsService.fail('token 获取失败,请传递 token 或书写正确');

    // 使用反射器,配合装饰器使用,获取装饰器传递过来的数据
    const authRoles = this.reflector.get<string[]>(
      'roles',
      context.getHandler(),
    );

    // 如果没有使用 roles 装饰,就获取不到值,就不鉴权,等于白名单
    if (!authRoles) return true;

    // 如果用户的所属角色与装饰器传递过来的值匹配则通过,否则不通过
    const userRoles = user.roles;
    for (let i = 0; i < userRoles.length; i++) {
      if (authRoles.includes(userRoles[i].rolename)) {
        return true;
      }
    }
    return false;
  }
}

简单测试

两个用户,分别对应不同的角色,分别请求 user 的 findOne 接口 用户 1:销售部员工和 admin 用户 2:人事部员工

用户 1:销售部员工和 admin
{
    "status": 200,
    "message": "请求成功",
    "data": {
        "user_id": 1,
        "username": "admin",
        "department_id": 1,
        "departmentname": "销售部",
        "roles": [
            {
                "id": 1,
                "rolename": "销售部员工"
            },
            {
                "id": 5,
                "rolename": "admin"
            }
        ]
    }
}

用户 2:人事部员工
{
    "status": 200,
    "message": "请求成功",
    "data": {
        "user_id": 2,
        "username": "admin2",
        "department_id": 2,
        "departmentname": "人事部",
        "roles": [
            {
                "id": 3,
                "rolename": "人事部员工"
            }
        ]
    }
}


不出意外的话:2 号用户的请求结果
{
    "status": 403,
    "message": "Forbidden resource",
    "error": "Forbidden",
    "path": "/user/1",
    "timestamp": "2021-05-21T14:44:04.954Z"
}

前端的权限访问则是通过权限表 url 和 type 来处理

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文