一切有为法 ✨ 应作如是观 ☀️|

sy0313

园龄:4年9个月粉丝:10关注:1

RBAC权限控制 (Role Based Access Control)

ACL和RBAC对比

之前的ACL权限控制是直接给用户分配权限的。

而RBAC是这样的:

RBAC是先分开角色,然后把角色分给指定的用户
通过在用户和权限之间多加一层“角色”来做权限管理

给角色分配权限,然后给用户分配角色
这样有什么好处呢?

 
 

比如说:
管理员有 a、b、c 3个权限,而张三李四王五都是管理员。
有一天想给管理员添加一个d权限
如果是ACL,则需要给张三、李四、王五分别分配这权限

而RBAC则只需要给张三、李四、王五分配管理员的角色,然后值更改管理员角色对应的权限就好了。

 
 
所以说,当用户很多的时候,给不同的用户分配不同的权限会很麻烦,这时候,使用RBAC来控制权限则更加方便。

 
 

nest.js实现RBAC权限控制

 

创建rbac_test的数据库:

CREATE DATABASE rbac_test DEFAULT CHARACTER SET utf8mb4;

创建nest项目

nest new rbac-test -p npm

安装typeorm的依赖

npm install --save @nestjs/typeorm typeorm mysql2

创建模块及编写代码

然后在AppModule引入TypeOrmModule:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [ 
    TypeOrmModule.forRoot({
      type: "mysql",
      host: "localhost",
      port: 3306,
      username: "root",
      password: "root",
      database: "rbac_test",
      synchronize: true,
      logging: true,
      entities: [],
      poolSize: 10,
      connectorPackage: 'mysql2',
      extra: {
          authPlugin: 'sha256_password',
      }
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 
然后添加创建 user 模块:

nest g resource user

添加User、Role、Permission的Entity:

用户、角色、权限都是多对多的关系:

user.entity.ts:

import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Role } from "./role.entity";

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id:number;

    @Column({length:50})
    username:string;

    @Column({length:50})
    password:string;

    @CreateDateColumn()
    updateTime:Date;

    @ManyToMany(()=>Role)
    @JoinTable({name:"user_role_relation"})
    roles:Role[];
}

User有id、username、password、createTime、updateTime五个字段;
通过@ManyToMany映射和Role的多对多关系,并指定中间表的名字;

role.entity.ts:

import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Permission } from "./permission.entity";

@Entity()
export class Role {
    @PrimaryGeneratedColumn()
    id:number;

    @Column({length:20})
    name:string;

    @CreateDateColumn()
    createTime:Date;

    @UpdateDateColumn()
    updateTime:Date;

    @ManyToMany(()=>Permission)
    @JoinTable({name:"role_permission_relation"})
    permissions:Permission[];
}

Role有id、name、createTime、updateTime 4个字段;
通过@ManyToMany映射和Permission的多对多关系,并指定中间表的名字;

permission.entity.ts:

import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";

@Entity()
export class Permission {
    @PrimaryGeneratedColumn()
    id:number;

    @Column({length:50})
    name:string;

    @Column({length:50,nullable:true})
    desc:string;

    @CreateDateColumn()
    createTime:Date;

    @UpdateDateColumn()
    updateTime:Date;
}

Permission有id、name、createTime、updateTime 4个字段。

 

然后再TypeOrm.forRoot的entities数组中加入这三个entity:

 
把nest服务跑起来试试:

npm run start:dev


可以看到正确生成了我们的定义的表结构:

  1. 生成了user、role、permission这三个表
  2. 生成了user_roole_relation、role_permission_relation 这 2 个中间表,和对应的外键约束

 
然后我们来添加一些数据:
修改下UserService,添加如下部分代码:

@InjectEntityManager()
entityManager: EntityManager;

async initData() {
    const user1 = new User();
    user1.username = '张三';
    user1.password = '111111';

    const user2 = new User();
    user2.username = '李四';
    user2.password = '222222';

    const user3 = new User();
    user3.username = '王五';
    user3.password = '333333';

    const role1 = new Role();
    role1.name = '管理员';

    const role2 = new Role();
    role2.name = '普通用户';

    const permission1 = new Permission();
    permission1.name = '新增 aaa';

    const permission2 = new Permission();
    permission2.name = '修改 aaa';

    const permission3 = new Permission();
    permission3.name = '删除 aaa';

    const permission4 = new Permission();
    permission4.name = '查询 aaa';

    const permission5 = new Permission();
    permission5.name = '新增 bbb';

    const permission6 = new Permission();
    permission6.name = '修改 bbb';

    const permission7 = new Permission();
    permission7.name = '删除 bbb';

    const permission8 = new Permission();
    permission8.name = '查询 bbb';


    role1.permissions = [
      permission1,
      permission2,
      permission3,
      permission4,
      permission5,
      permission6,
      permission7,
      permission8
    ]

    role2.permissions = [
      permission1,
      permission2,
      permission3,
      permission4
    ]

    user1.roles = [role1];

    user2.roles = [role2];

    await this.entityManager.save(Permission, [
      permission1, 
      permission2,
      permission3,
      permission4,
      permission5,
      permission6,
      permission7,
      permission8
    ])

    await this.entityManager.save(Role, [
      role1,
      role2
    ])

    await this.entityManager.save(User, [
      user1,
      user2
    ])  
}

然后再UserController里添加一个handler:

@Get('init')
async initData() {
    await this.userService.initData();
    return 'done';
}

然后把nest服务跑起来;
访问一下user/init,发现数据库中已经正确的插入了数据;

 

然后实现一下登录逻辑,通过jwt的方式保存登录信息。

在userController中增加一个login的handler:

@Post('login')
login(@Body() loginUser: UserLoginDto){
    console.log(loginUser)
    return 'success'
}

添加user/dto/user-login.dto.ts:

export class UserLoginDto {
  username: string;
  password: string;
}

安装参数验证所需要的包:

npm install --save class-validator class-transformer

然后给dto对象添加class-validator的装饰器:

import { IsNotEmpty, Length } from "class-validator";

export class UserLoginDto {
    @IsNotEmpty()
    @Length(1, 50)
    username: string;

    @IsNotEmpty()
    @Length(1, 50)
    password: string;
}

全局启用一下ValidationPipe:

使用接口测试工具测试一下,发现对username和password确实做了验证。

接下来实现查询数据库的逻辑,在UserService中添加login方法:

async login(loginUserDto: UserLoginDto) {
    const user = await this.entityManager.findOne(User, {
      where: {
        username: loginUserDto.username
      },
      relations: {
        roles: true
      }
    });

    if(!user) {
      throw new HttpException('用户不存在', HttpStatus.ACCEPTED);
    }

    if(user.password !== loginUserDto.password) {
      throw new HttpException('密码错误', HttpStatus.ACCEPTED);
    }

    return user;
}

这里把user的roles也关联查询出来
我们在UserController中的login方法调用下试试:

@Post('login')
async login(@Body() loginUser: UserLoginDto){
    const user = await this.userService.login(loginUser);

    console.log(user);

    return 'success'
}

访问一下login接口:

可以看到 user的信息和roles信息都查询出来了。

我们要把user的信息放到jwt中去,所以安装一下相关的包:

npm install --save @nestjs/jwt

然后在AppModule里引入JwtModule:

设置为全局模块,这样不用每个模块都引入

然后在UserController里注入JwtService:

把 user 信息放到 jwt 里,然后返回:

使用接口测试工具测试下:

服务端在登陆后返回了jwt的token,然后在请求时带上这个token才能访问一些资源

 
 
我们添加aaa,bbb两个模块,分别生成CRUD方法:

nest g resource aaa 
nest g resource bbb 

现在这两个接口可以直接访问

我们为这两个接口添加一些权限控制
管理员的角色有aaa、bbb的增删改查权限;
而普通的用户只用bbb的增删改查权限;

先添加一个loginGuard,限制只有登录状态才可以访问这些接口:

nest g guard login --no-spec --flat

在这个生成的守卫中增加登录状态的检查:

import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { Observable } from 'rxjs';

@Injectable()
export class LoginGuard implements CanActivate {
  
  @Inject(JwtService)
  private jwtService: JwtService;
  
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest();
    
    const authorization = request.headers.authorization;

    if(!authorization) {
      throw new UnauthorizedException('用户未登录');
    }

    try{
      const token = authorization.split(' ')[1];
      const data = this.jwtService.verify(token);

      return true;
    } catch(e) {
      throw new UnauthorizedException('token 失效,请重新登录');
    }
  }
}

这里不用查数据库了,因为 jwt 是用密钥加密的,只要 jwt 能 verify 通过就行了。

然后把它放到 request 上
但是这时候会报错user不在Request的类型上
扩展下Request的类型就好了:

因为typescript里同名module和interface会自动合并,可以这样扩展类型。

我们直接全局加上登录守卫:

通过 app.userGlobalXxx 的方式不能注入 provider
可以通过在 AppModule 添加 token 为 APP_XXX 的 provider 的方式来声明全局 Guard、Pipe、Intercepter 等

 

再访问一下/aaa/bbb接口,发现需要登录验证了
 
但这时候你访问 /user/login 接口也被拦截了
我们需要区分哪些接口需要登录,哪些接口不需要。

这时候就需要用到SetMetadata
我们添加custom-decorator.ts来放自定义的装饰器:

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

export const RequireLogin = () => SetMetadata('require-login', true);

声明一个RequireLogin的装饰器。

在aaa、bbb的controller上使用一下:


直接在controller上添加声明,不需要再每个handler都添加,这样方便的多。

然后改造一下LoginGuard,取出目标handler的metadata来判断是否需要登录:

如果目标handler或者controller不包含require-login的metadata,那就放行,否则才检查jwt。

现在登录接口能够正常访问:

而且aaa、bbb是需要登录的:

因为他们包含require-login的metadata

然后我们带上token登陆,访问一下/aaa或者/bbb:

可以顺利访问:

然后我们再进一步控制权限:

我们完成了登录控制
还需要完成当前登录用户的权限控制,所以再写一个PermissionGuard:

nest g guard permission --no-spec --flat

同样声明成全局Guard:

PermissionGuard里需要用到UserService,所以在UserModule中导出下UserService:

在PermissionGuard中注入UserService:

import { CanActivate, ExecutionContext, Injectable,Inject } from '@nestjs/common';
import { Observable } from 'rxjs';
import { UserService } from './user/user.service';

@Injectable()
export class PermissionGuard implements CanActivate {
  @Inject(UserService)
  private userService:UserService;

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log("userService-----",this.userService);
    return true;
  }
}

然后再userService里实现查询role的信息的service:

  async findRolesByIds(roleIds: number[]) {
    return this.entityManager.find(Role, {
      where: { id: In(roleIds) },
      relations: { permissions: true },
    });
  }

关联查询出permissions:
然后在PermissionGuard里调用下:

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  Inject,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request } from 'express';
import { UserService } from './user/user.service';
import { Permission } from './user/entities/permission.entity';

@Injectable()
export class PermissionGuard implements CanActivate {
  @Inject(UserService)
  private userService: UserService;

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request: Request = context.switchToHttp().getRequest();

    if (!request.user) {
      return true;
    }

    const roles = await this.userService.findRolesByIds(
      request.user.roles.map((item) => item.id),
    );

    const permissions: Permission[] = roles.reduce((total, current) => {
      total.push(...current.permissions);
      return total;
    }, []);

    console.log(permissions);
  }
}

因为PermissionGuard在LoginGuard之后调用(在AppModule里声明在LoginGuard之后)
所以走到这里request里就有user对象了。

但也不一定,因为LoginGuard没有登录也可能放行,所以要判断下request.user如果没有,这里也放行
然后取出user的roles的id,查出roles的permission信息,然后合并到一个数组里。

我们试试看:
带上token访问:

可以看到打印了这个用户拥有的角色所有的权限:

 

再增加自定义decorator:

然后我们再BbbController上声明需要的权限:

在PermissionGuard里取出来判断:

带上token,访问一下/bbb:
可以看到打印了用户拥有的permission 还有接口需要的permission

这两个一对比,不就知道有没有权限访问这个接口了吗

添加这样的对比逻辑:

for (let i = 0; i < requiredPermissions.length; i++) {
      const curPermission = requiredPermissions[i];
      const found = permissions.find((item) => item.name === curPermission);

      if (!found) {
        throw new UnauthorizedException('您没有权限访问该接口');
      }
    }

当前的用户是李四,是没有访问bbb的权限的:

我们再登录一下张三账号,拷贝一下返回来的token,再访问一下bbb接口:

就可以正常访问了,因为张三是管理员,拥有全部的权限。

 
这样,我们就实现了基于 RBAC 的权限控制。

本文作者:sy0313

本文链接:https://www.cnblogs.com/sunyan97/p/17878916.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   sy0313  阅读(119)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起