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
可以看到正确生成了我们的定义的表结构:
- 生成了user、role、permission这三个表
- 生成了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 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步