前言
一般后台的系统都是需要权限控制的,而且最常用的就是 rbac
权限控制,这里我们就来看看如何在 nest
中使用 rbac
进行权限控制。rbac
的权限控制主要分为三个部分,用户、角色、权限,用户和角色是多对多的关系,角色和权限也是多对多的关系,用户和权限是多对多的关系,这里我们就来看看如何在 nest
中使用 rbac
进行权限控制。
由上图可以看出,先把权限分配给角色,然后在把角色分配给用户,这样就可以实现权限控制了。如果想要修改一个用户的权限,只需要修改角色的权限就可以了,这样就可以实现权限的统一管理。那就在 nest
简单实践先 rabc
权限吧。
创建数据库
首先我们需要创建数据库,这里我们使用 mysql
数据库,创建数据库的 sql
语句如下:
sql
CREATE DATABASE `nest_rbac` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
创建项目
创建项目的命令如下:
bash
nest new nest-rbac
安装 typorm 相关依赖
bash
npm install --save @nestjs/typeorm typeorm mysql2
配置 typeorm
相关配置,打开app.module.ts
文件:
typescript
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'nest_rbac',
synchronize: true,
logging: true,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
poolSize: 10,
connectorPackage: 'mysql2'
})
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
创建用户模块
bash
nest g resource user
数据库表之间的关系如图所示:
然后添加用户、角色、权限的实体类,在user/entity
文件夹下添加:
- user.entity.ts
typescript
import {
Column,
CreateDateColumn,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
UpdateDateColumn
} 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()
createTime: Date
@UpdateDateColumn()
updateTime: Date
@ManyToMany(() => Role)
@JoinTable({
name: 'user_role_relation'
})
roles: Role[]
}
通过@ManyToMany
装饰器来表示多对多的关系,然后通过@JoinTable
来指定关联表user_role_relation
。
role.entity.ts
typescript
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[]
}
通过@ManyToMany
装饰器来表示多对多的关系,然后通过@JoinTable
来指定关联表role_permission_relation
。
permission.entity.ts
typescript
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
@Entity()
export class Permission {
@PrimaryGeneratedColumn()
id: number
@Column({
length: 50
})
name: string
@Column({
length: 100,
nullable: true
})
desc: string
@CreateDateColumn()
createTime: Date
@UpdateDateColumn()
updateTime: Date
}
然后重新运行项目,会自动创建表。这样一个初始化项目就建立好了,接下来开始实现 rbac
权限控制。首先先对数据库添加一些测试数据,通过定义一个接口方法来添加。在user/user.service.ts
文件中添加一个 initData
方法来初始化数据:
typescript
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { InjectEntityManager } from '@nestjs/typeorm'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { EntityManager, In } from 'typeorm'
import { User } from './entities/user.entity'
import { Role } from './entities/role.entity'
import { Permission } from './entities/permission.entity'
import { LoginUserDto } from './dto/login-user.dto'
@Injectable()
export class 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])
}
async login(loginUserDto: LoginUserDto) {
const user = await this.entityManager.findOne(User, {
where: {
username: loginUserDto.username
},
relations: ['roles']
})
if (!user) {
throw new HttpException('用户不存在', HttpStatus.ACCEPTED)
}
if (user.password !== loginUserDto.password) {
throw new HttpException('密码错误', HttpStatus.ACCEPTED)
}
return user
}
async findRolesByIds(roleIds) {
return this.entityManager.find(Role, {
where: {
id: In(roleIds)
},
relations: {
permissions: true
}
})
}
findOne(id: number) {
return `This action returns a #${id} user`
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`
}
remove(id: number) {
return `This action removes a #${id} user`
}
}
以上初始化的 rbac 权限如图所示:
定义一个 user
的控制器路由来初始化数据,在user/user.controller.ts
文件中添加:
typescript
@Get('init')
async initData() {
await this.userService.initData();
return 'done';
}
直接调用初始化数据接口,然后就可以在数据库中看到初始化的数据了。
使用 jwt 实现登录
关于登录就不展开说了,如果还不明白的可以看下之前的 jwt
文章,这里直接使用 jwt
实现登录。首先定义下登录的路由,在user/user.controller.ts
文件中添加:
typescript
@Post('login')
async login(@Body() loginUserDto: LoginUserDto) {
const user = await this.userService.login(loginUserDto);
const payload = {
username: user.username,
sub: user.id,
roles: user.roles.map(role => role.name),
permissions: user.roles.map(role => role.permissions.map(permission => permission.name)).flat()
};
return {
access_token: this.jwtService.sign(payload)
};
}
在 dto
文件夹下需要对参数进行校验,需要安装class-validator
和class-transformer
依赖:
bash
npm install --save class-validator class-transformer
然后在user/dto
文件夹下添加login-user.dto.ts
文件:
typescript
import { IsNotEmpty } from 'class-validator'
export class LoginUserDto {
@IsNotEmpty()
@Length(1, 50)
username: string
@IsNotEmpty()
@Length(1, 50)
password: string
}
然后在user/user.service.ts
文件中添加 login
方法:
typescript
async login(loginUserDto: LoginUserDto) {
const user = await this.entityManager.findOne(User, {
where: {
username: loginUserDto.username
},
relations: ['roles']
});
if (!user) {
throw new HttpException('用户不存在', HttpStatus.ACCEPTED);
}
if (user.password !== loginUserDto.password) {
throw new HttpException('密码错误', HttpStatus.ACCEPTED);
}
return user;
}
需要全局开启验证,打开main.ts
文件:
typescript
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// 开启全局验证
app.useGlobalPipes(new ValidationPipe())
await app.listen(3000)
}
bootstrap()
需要把 user
的信息放到 jwt
,安装@nestjs/jwt
依赖:
bash
npm install --save @nestjs/jwt
然后在app.module.ts
文件中添加:
typescript
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { UserModule } from './user/user.module'
import { JwtModule } from '@nestjs/jwt'
@Module({
imports: [
JwtModule.register({
global: true,
secret: 'water',
signOptions: {
expiresIn: '1d'
}
})
]
})
设置为全局模块,然后各个模块都可以使用了。
tsx
@Post('login')
async login(@Body() loginUser: UserLoginDto){
const user = await this.userService.login(loginUser);
const token = this.jwtService.sign({
user: {
username: user.username,
roles: user.roles
}
});
return {
token
}
}
这样就通过 jwt
实现了登录,然后就可以通过 token
来获取用户信息了。但是现在有一个问题就说所有的接口都需要登录鉴权才能访问了,其实有些接口是不需要登录鉴权的,比如登录接口,这里我们就需要对接口进行分类,然后对不同的接口进行不同的鉴权。这里我们就需要使用到 nest
的路由守卫了。
使用路由守卫实现鉴权
使用 cli
创建一个路由守卫:
bash
nest g guard login --no-spec --flat
在login.guard.ts
文件中修改判断逻辑:
typescript
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Request } from 'express'
import { Observable } from 'rxjs'
declare module 'express' {
interface Request {
user: {
username: string
roles: Role[]
}
}
}
@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)
request.user = data.user
return true
} catch (e) {
throw new UnauthorizedException('token 失效,请重新登录')
}
}
}
如果在每一个需要鉴权的路由上加守卫,有点太麻烦了,这里可以直接在全局加上守卫。在 app.module.ts
文件中添加:
typescript
providers: [
{
provide: APP_GUARD,
useClass: LoginGuard
}
]
这样这个守卫就会对所有的路由进行鉴权了,但是这样就会有一个问题,就是登录接口也会被鉴权,这样就会导致登录接口无法访问了,这里我们就需要对登录接口进行排除。先声明一个自定义装饰器,来标识哪些接口需要鉴权。
typescript
import { SetMetadata } from '@nestjs/common'
export const RequireLogin = () => SetMetadata('require-login', true)
然后需要改造下 LoginGuard
,来判断是否需要鉴权。
typescript
const requireLogin = this.reflector.getAllAndOverride('require-login', [
context.getClass(),
context.getHandler()
])
console.log(requireLogin)
if (!requireLogin) {
return true
}
这样登录鉴权的功能就实现了,但是这里还有一个问题,就是如果用户没有权限访问某个接口,这里就需要对接口进行权限控制了。创建一个权限守卫:
bash
nest g guard permission --no-spec --flat
在permission.guard.ts
文件中添加:
typescript
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common'
import { Request } from 'express'
import { Observable } from 'rxjs'
import { UserService } from './user.service'
@Injectable()
export class PermissionGuard implements CanActivate {
@Inject(UserService)
private userService: UserService
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
console.log(this.userService)
return true
}
}
在 userservice
中添加一个方法,来查询角色信息:
typescript
async findRolesByIds(roleIds: number[]) {
return this.entityManager.find(Role, {
where: {
id: In(roleIds)
},
relations: {
permissions: true
}
});
}
然后在permission.guard.ts
文件中添加:
typescript
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common'
import { Request } from 'express'
import { UserService } from './user/user.service'
@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)
return true
}
}
这样就可以获取到用户的权限了,然后就可以对接口进行权限控制了。这里我们就需要在自定义装饰器中添加一个权限的标识。
typescript
export const RequirePermission = (permissions: string[]) =>
SetMetadata('require-permission', permissions)
然后在permission.guard.ts
文件中添加:
typescript
const requirePermission = this.reflector.getAllAndOverride<string[]>('require-permission', [
context.getClass(),
context.getHandler()
])
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('您没有访问该接口的权限')
}
}
小结
通过以上的实践,我们就可以在 nest
中使用 rbac
进行权限控制了,这里只是简单实践了下。在业务中使用还需要多多练习。