夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node基础

  • 《MySQL》学习笔记

  • Midway

  • Nest

    • 开篇词
    • 学习理由
    • nest概念扫盲
    • 快速掌握 nestcli
    • 5种http数据传输方式
    • IoC 解决了什么痛点问题?
    • 如何调试 Nest 项目
    • Provider注入对象
    • 全局模块和生命周期
    • AOP 架构有什么好处?
    • 一网打尽 Nest 全部装饰器
    • Nest如何自定义装饰器
    • Metadata和Reflector
    • ExecutionContext切换上下文
    • Module和Provider的循环依赖处理
    • 如何创建动态模块
    • Nest和Express,fastify
    • Nest的Middleware
    • RxJS和Interceptor
    • 内置Pipe和自定义Pipe
    • ValidationPipe验证post请求参数
    • 如何自定义 Exception Filter
    • 图解串一串 Nest 核心概念
    • 接口如何实现多版本共存
    • Express如何使用multer实现文件上传
    • Nest使用multer实现文件上传
    • 图书管理系统
    • 大文件分片上传
    • 最完美的 OSS 上传方案
    • Nest里如何打印日志
    • 为什么Node里要用Winston打印日志
    • Nest 集成日志框架 Winston
    • 通过Desktop学Docker也太简单了
    • 你的第一个 Dockerfile
    • Nest 项目如何编写 Dockerfile
    • 提升 Dockerfile 水平的 5 个技巧
    • Docker 是怎么实现的
    • 为什么 Node 应用要用 PM2 来跑?
    • 快速入门 MySQL
    • SQL 查询语句的所有语法和函数
    • 一对一、join 查询、级联方式
    • 一对多、多对多关系的表设计
    • 子查询和 EXISTS
    • SQL 综合练习
    • MySQL 的事务和隔离级别
    • MySQL 的视图、存储过程和函数
    • Node 操作 MySQL 的两种方式
    • 快速掌握 TypeORM
    • TypeORM 一对一的映射和关联 CRUD
    • TypeORM 一对多的映射和关联 CRUD
    • TypeORM 多对多的映射和关联 CRUD
    • 在 Nest 里集成 TypeORM
    • TypeORM保存任意层级的关系
    • 生产环境为什么用TypeORM的migration迁移功能
    • Nest 项目里如何使用 TypeORM 迁移
    • 如何动态读取不同环境的配置?
    • 快速入门 Redis
    • 在 Nest 里操作 Redis
    • 为什么不用 cache-manager 操作 Redis
    • 两种登录状态保存方式:JWT、Session
    • Nest 里实现 Session 和 JWT
    • MySQL + TypeORM + JWT 实现登录注册
    • 基于 ACL 实现权限控制
      • 这节我们就来实现下 ACL 的权限控制。
      • 添加 User 和 Permission 的 Entity:
        • 通过 @ManyToMany 声明和 Permisssion 的多对多关系。
        • 然后我们插入一些数据,不用 sql 插入,而是用 TypeORM 的 api 来插入:
        • 然后我们再实现登录的接口,这次通过 session + cookie 的方式。
        • 接下来实现查询数据库的逻辑,在 UserService 添加 login 方法:
        • 登录成功之后会返回 cookie,之后只要带上这个 cookie 就可以查询到服务端的对应的 session,从而取出 user 信息。
      • 先添加一个 LoginGuard,限制只有登录状态才可以访问这些接口:
        • 然后增加登录状态的检查:
        • 然后给接口都加上这个 Guard:
        • 你访问登录接口之后,服务端返回 set-cookie 的 header,postman 会自动带上 cookie,不需要手动带:
      • 但是这样还不够,我们还需要再做登录用户的权限控制,所以再写个 PermissionGuard:
        • 因为 PermissionGuard 里需要用到 UserService 来查询数据库,所以把它移动到 UserModule 里:
        • 我们在 AaaModule 里引入这个 UserModule:
      • postman 访问下:
        • 首先重新登录,post 方式请求 /user/login:
        • 然后 get 访问 /aaa,postman 会自动带上 cookie。
      • 然后来实现权限检查的逻辑。
        • 根据用户名查找用户,并且查询出关联的权限来。
        • 先登录,拿到 cookie:
        • 然后请求 /aaa 接口:
        • 然后我们就根据当前 handler 需要的权限来判断是否返回 true 就可以了。
        • 我们试一下:
      • 每次访问接口,都会触发这样 3 个表的关联查询。
        • 怎么优化一下呢?
        • 这时就需要 redis 了,redis 的缓存就是用来做这种优化的。
        • 先查询 redis、没有再查数据库并存到 redis,有的话就直接用 redis 的缓存结果。
        • 这里如果你没跑 redis server,需要先通过 docker 把它跑起来。
      • 总结
    • 基于 RBAC 实现权限控制
    • access_token和refresh_token实现无感登录
    • 单token无限续期实现登录无感刷新
    • 使用 passport 做身份认证
    • passport 实现 GitHub 三方账号登录
    • passport 实现 Google 三方账号登录
    • 为什么要使用 Docker Compose ?
    • Docker 容器通信的最简单方式:桥接网络
    • Docker 支持重启策略,是否还需要 PM2
    • 快速掌握 Nginx 的 2 大核心用法
    • 基于 Nginx 实现灰度系统
    • 基于 Redis 实现分布式 session
    • Redis + 高德地图,实现附近的充电宝
    • 用 Swagger 自动生成 api 文档
    • 如何灵活创建 DTO
    • class- validator 的内置装饰器,如何自定义装饰器
    • 序列化 Entity,你不需要 VO 对象
    • 手写序列化 Entity 的拦截器
    • 使用 compodoc 生成文档
    • Node 如何发邮件?
    • 实现基于邮箱验证码的登录
    • 基于 sharp 实现 gif 压缩工具
    • 大文件如何实现流式下载?
    • Puppeteer 实现爬虫,爬取 BOSS 直聘全部前端岗位
    • 实现扫二维码登录
    • Nest 的 REPL 模式
    • 实现 Excel 导入导出
    • 如何用代码动态生成 PPT
    • 如何拿到服务器 CPU、内存、磁盘状态
    • Nest 如何实现国际化?
    • 会议室预订系统:需求分析和原型图
    • 会议室预订系统:技术方案和数据库设计
    • 会议室预订系统:用户管理模块--用户注册
    • 会议室预订系统:用户管理模块--配置抽离、登录认证鉴权
    • 会议室预订系统:用户管理模块-- interceptor、修改信息接口
    • 会议室预订系统:用户管理模块--用户列表和分页查询
    • 会议室预订系统:用户管理模块-- swagger 接口文档
    • 会议室预订系统:用户管理模块-- 用户端登录注册页面
    • 会议室预订系统:用户管理模块-- 用户端信息修改页面
    • 会议室预订系统:用户管理模块-- 头像上传
    • 会议室预订系统:用户管理模块-- 管理端用户列表页面
    • 会议室预订系统:用户管理模块-- 管理端信息修改页面
    • 会议室预订系统:会议室管理模块-后端开发
    • 会议室预订系统:会议室管理模块-管理端前端开发
    • 会议室预订系统:会议室管理模块-用户端前端开发
    • 会议室预订系统:预定管理模块-后端开发
    • 会议室预订系统:预定管理模块-管理端前端开发
    • 会议室预订系统:预定管理模块-用户端前端开发
    • 会议室预订系统:统计管理模块-后端开发
    • 会议室预订系统:统计管理模块-前端开发
    • 会议室预订系统:后端项目部署到阿里云
    • 会议室预订系统:前端项目部署到阿里云
    • 会议室预定系统:用 migration 初始化表和数据
    • 会议室预定系统:文件上传 OSS
    • 会议室预定系统:Google 账号登录后端开发
    • 会议室预定系统:Google 账号登录前端开发
    • 会议室预定系统:后端代码优化
    • 会议室预定系统:集成日志框架 winston
    • 会议室预定系统:前端代码优化
    • 会议室预定系统:全部功能测试
    • 会议室预定系统:项目总结
    • Nest 如何创建微服务?
    • Nest 的 Monorepo 和 Library
    • 用 Etcd 实现微服务配置中心和注册中心
    • Nest 集成 Etcd 做注册中心、配置中心
    • 用 Nacos 实现微服务配置中心和注册中心
    • 基于 gRPC 实现跨语言的微服务通信
    • 快速入门 ORM 框架 Prisma
    • Prisma 的全部命令
    • Prisma 的全部 schema 语法
    • Primsa Client 单表 CRUD 的全部 api
    • Prisma Client 多表 CRUD 的全部 api
    • 在 Nest 里集成 Prisma
    • 为什么前端监控系统要用 RabbitMQ?
    • 基于 Redis 实现关注关系
    • 基于 Redis 实现各种排行榜(周榜、月榜、年榜)
    • 考试系统:需求分析
    • 考试系统:技术方案和数据库设计
    • 考试系统:微服务、Lib 拆分
    • 考试系统;用户注册
    • 考试系统:用户登录、修改密码
    • 考试系统:考试微服务
    • 考试系统:登录、注册页面
    • 考试系统:修改密码、试卷列表页面
    • 考试系统:新增试卷、回收站
    • 考试系统:试卷编辑器
    • 考试系统:试卷回显、预览、保存
    • 考试系统:答卷微服务
    • 考试系统:答题页面
    • 考试系统:自动判卷
    • 考试系统:分析微服务、排行榜页面
    • 考试系统:整体测试
    • 考试系统:项目总结
    • 用 Node.js 手写 WebSocket 协议
    • Nest 开发 WebSocket 服务
    • 基于 Socket.io 的 room 实现群聊
    • 聊天室:需求分析和原型图
    • 聊天室:技术选型和数据库设计
    • 聊天室:用户注册
    • 聊天室:用户登录
    • 聊天室:修改密码、修改信息
    • 聊天室:好友列表、发送好友申请
    • 聊天室:创建聊天室、加入群聊
    • 聊天室:登录、注册页面开发
    • 聊天室:修改密码、信息页面开发
    • 聊天室:头像上传
    • 聊天室:好友∕群聊列表页面
    • 聊天室:添加好友弹窗、通知页面
    • 聊天室:聊天功能后端开发
    • 聊天室:聊天功能前端开发
    • 聊天室:一对一聊天
    • 聊天室:创建群聊、进入群聊
    • 聊天室:发送表情、图片、文件
    • 聊天室:收藏
    • 聊天室:全部功能测试
    • 聊天室:项目总结
    • MongoDB 快速入门
    • 使用 mongoose 操作 MongoDB 数据库
    • GraphQL 快速入门
    • Nest 开发 GraphQL 服务:实现 CRUD
    • GraphQL + Primsa + React 实现 TodoList
    • 如何调试 Nest 源码?
  • 其他

  • 服务端
  • Nest
夜猫子
2025-03-10
目录

基于 ACL 实现权限控制

上节我们实现了注册和登录,有的接口只有登录可以访问,会在 Guard 里做身份验证(Authentication)。

image.png

但有的接口,不只需要登录,可能还需要一定的权限,这时就需要鉴权(Authorization)。

比如管理员登录后,可以调用用户管理的接口,但普通用户登录后就不可以。

image.png

也就是说,身份验证通过之后还需要再做一步权限的校验,也就是鉴权。

这俩单词也比较相似:身份验证(Authentication)、鉴权(Authorization)。

那怎么给不同用户分配权限呢?

最简单的方式自然是直接给用户分配权限:

比如用户 1 有权限 A、B、C,用户 2 有权限 A,用户 3 有权限 A、B。

这种记录每个用户有什么权限的方式,叫做访问控制表(Access Control List)

用户和权限是多对多关系,存储这种关系需要用户表、角色表、用户-角色的中间表。

# 这节我们就来实现下 ACL 的权限控制。

在数据库中创建 acl_test 的 database。

CREATE DATABASE acl_test DEFAULT CHARACTER SET utf8mb4;
1

刷新可以看到这个 database:

创建个 nest 项目:

nest new acl-test -p npm
1

安装 typeorm 的依赖:

npm install --save @nestjs/typeorm typeorm mysql2
1

在 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: "guang",
      database: "acl_test",
      synchronize: true,
      logging: true,
      entities: [],
      poolSize: 10,
      connectorPackage: "mysql2",
      extra: {
        authPlugin: "sha256_password",
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

然后添加创建 user 模块:

nest g resource user
1

# 添加 User 和 Permission 的 Entity:

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

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

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

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

  @CreateDateColumn()
  createTime: Date;

  @UpdateDateColumn()
  updateTime: Date;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

User 有 id、username、password、createTime、updateTime 5 个字段。

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

permission 有 id、name、desc、createTime、updateTime 5 个字段,desc 字段可以为空。

然后在 User 里加入和 Permission 的关系,也就是多对多:

image.png

@ManyToMany(() => Permission)
@JoinTable({
    name: 'user_permission_relation'
})
permissions: Permission[]
1
2
3
4
5

# 通过 @ManyToMany 声明和 Permisssion 的多对多关系。

多对多是需要中间表的,通过 @JoinTable 声明,指定中间表的名字。

然后在 TypeOrm.forRoot 的 entities 数组加入这俩 entity:

把 Nest 服务跑起来试试:

npm run start:dev
1

可以看到生成了 user、permission、user_permission_relation 这 3 个表。

并且中间表 user_permission_relation 还有 userId、permissionId 两个外键。

image.png

image.png

image.png

image.png

可以看到,3 个表生成的都是对的,并且中间表的两个外键也都是主表删除或者更新时,从表级联删除或者更新。

# 然后我们插入一些数据,不用 sql 插入,而是用 TypeORM 的 api 来插入:

修改下 UserService,添加这部分代码:

@InjectEntityManager()
entityManager: EntityManager;

async initData() {
    const permission1 = new Permission();
    permission1.name = 'create_aaa';
    permission1.desc = '新增 aaa';

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

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

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

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

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

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

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

    const user1 = new User();
    user1.username = '东东';
    user1.password = 'aaaaaa';
    user1.permissions  = [
      permission1, permission2, permission3, permission4
    ]

    const user2 = new User();
    user2.username = '光光';
    user2.password = 'bbbbbb';
    user2.permissions  = [
      permission5, permission6, permission7, permission8
    ]

    await this.entityManager.save([
      permission1,
      permission2,
      permission3,
      permission4,
      permission5,
      permission6,
      permission7,
      permission8
    ])
    await this.entityManager.save([
      user1,
      user2
    ]);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

注入 EntityManager,实现权限和用户的保存。

aaa 增删改查、bbb 增删改查,一个 8 个权限。

user1 有 aaa 的 4 个权限,user2 有 bbbb 的 4 个权限。

调用 entityManager.save 来保存。

然后改下 UserController:

@Get('init')
async initData() {
    await this.userService.initData();
    return 'done'
}
1
2
3
4
5

添加 init 的路由。

浏览器访问下:

服务端打印了一堆 sql,包了一层事务:

分别向 user、permission、user_permission_relation 中插入了数据。

我们在 mysql workbench 里刷新下:

permission 表插入了 8 条权限记录:

user 表插入了 2 条用户记录:

中间表插入了 8 条记录,两个用户各拥有 4 个权限:

# 然后我们再实现登录的接口,这次通过 session + cookie 的方式。

安装 session 相关的包:

npm install express-session @types/express-session
1

在 main.ts 里使用这个中间件:

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import * as session from "express-session";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(
    session({
      secret: "guang",
      resave: false,
      saveUninitialized: false,
    })
  );
  await app.listen(3000);
}
bootstrap();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

secret 是加密 cookie 的密钥。

resave 是 session 没变的时候要不要重新生成 cookie。

saveUninitialized 是没登录要不要也创建一个 session。

然后在 UserController 添加一个 /user/login 的路由:

@Post('login')
login(@Body() loginUser: LoginUserDto, @Session() session){
    console.log(loginUser)
    return 'success'
}
1
2
3
4
5

然后先去创建 dto 对象:

export class LoginUserDto {
  username: string;

  password: string;
}
1
2
3
4
5

安装 ValidationPipe 用到的包:

npm install --save class-validator class-transformer
1

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

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

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

  @IsNotEmpty()
  @Length(1, 50)
  password: string;
}
1
2
3
4
5
6
7
8
9
10
11

全局启用 ValidationPipe:

然后在 postman 里测试下:

ValidationPipe 不通过的时候,会返回错误信息:

ValidationPipe 通过之后,就会执行 handler 里的方法:

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

async login(loginUserDto: LoginUserDto) {
    const user = await this.entityManager.findOneBy(User, {
      username: loginUserDto.username
    });

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

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

    return user;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后改下 UserController 的 login 方法:

@Post('login')
async login(@Body() loginUser: LoginUserDto, @Session() session){
    const user = await this.userService.login(loginUser);

    session.user = {
      username: user.username
    }

    return 'success';
}
1
2
3
4
5
6
7
8
9
10

调用 userService,并且把 user 信息放入 session。

再用 postman 登录下:

用户不存在: image.png

密码错误:

image.png

登录成功: image.png

image.png

# 登录成功之后会返回 cookie,之后只要带上这个 cookie 就可以查询到服务端的对应的 session,从而取出 user 信息。

然后添加 aaa、bbb 两个模块,分别生成 CRUD 方法:

nest g resource aaa
nest g resource bbb
1
2

image.png

image.png

现在这些接口可以直接访问:

image.png

image.png

而实际上这些接口是要控制权限的。

用户东东有 aaa 的增删改查权限,而用户光光拥有 bbb 的增删改查权限。

所以要对接口的调用做限制。

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

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

# 然后增加登录状态的检查:

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

declare module 'express-session' {
  interface Session {
    user: {
      username: string
    }
  }
}

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

    if(!request.session?.user){
      throw new UnauthorizedException('用户未登录');
    }

    return true;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

因为默认的 session 里没有 user 的类型,所以需要扩展下:

image.png

利用同名 interface 会自动合并的特点来扩展 Session。

# 然后给接口都加上这个 Guard:

image.png

image.png

再访问下:

image.png

image.png

在 postman 里带上 cookie 访问:

image.png

# 你访问登录接口之后,服务端返回 set-cookie 的 header,postman 会自动带上 cookie,不需要手动带:

image.png

行为和浏览器里一致。

这时候再访问 aaa、bbb 的接口,就可以访问了:

image.png

image.png

# 但是这样还不够,我们还需要再做登录用户的权限控制,所以再写个 PermissionGuard:

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

# 因为 PermissionGuard 里需要用到 UserService 来查询数据库,所以把它移动到 UserModule 里:

image.png

注入 UserService:

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;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在 UserModule 的 providers、exports 里添加 UserService

import { Module, UseGuards } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
import { PermissionGuard } from "./permission.guard";

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}
1
2
3
4
5
6
7
8
9
10
11

# 我们在 AaaModule 里引入这个 UserModule:

然后在 /aaa 的 handler 里添加 PermissionGuard:

image.png

# postman 访问下:

# 首先重新登录,post 方式请求 /user/login:

image.png

# 然后 get 访问 /aaa,postman 会自动带上 cookie。

image.png

服务端打印了 UserService:

image.png

说明在 PermissionGuard 里成功注入了 UserService。

# 然后来实现权限检查的逻辑。

在 UserService 里添加一个方法:

async findByUsername(username: string) {
  const user = await this.entityManager.findOne(User, {
    where: {
      username,
    },
    relations: {
      permissions: true
    }
  });
  return user;
}
1
2
3
4
5
6
7
8
9
10
11

# 根据用户名查找用户,并且查询出关联的权限来。

在 PermissionGuard 里调用下:

import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } 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;

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

    const user = request.session.user;
    if(!user) {
      throw new UnauthorizedException('用户未登录');
    }

    const foundUser = await this.userService.findByUsername(user.username);

    console.log(foundUser);

    return true;
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

打印了下查找到的登录用户的信息。

我们试试看:

# 先登录,拿到 cookie:

# 然后请求 /aaa 接口:

服务端打印了当前用户的权限信息:

# 然后我们就根据当前 handler 需要的权限来判断是否返回 true 就可以了。

那怎么给当前 handler 标记需要什么权限呢?

很明显是通过 metadata。

给 /aaa 接口声明需要 query_aaa 的 permission。

然后在 PermissionGuard 里通过 reflector 取出来:

取出 handler 声明的 metadata,如果用户权限里包含需要的权限,就返回 true,否则抛出没有权限的异常。

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

@Injectable()
export class PermissionGuard implements CanActivate {

  @Inject(UserService)
  private userService: UserService;

  @Inject(Reflector)
  private reflector: Reflector;

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

    const user = request.session.user;
    if(!user) {
      throw new UnauthorizedException('用户未登录');
    }

    const foundUser = await this.userService.findByUsername(user.username);

    const permission = this.reflector.get('permission', context.getHandler());

    if(foundUser.permissions.some(item => item.name === permission)) {
       return true;
    } else {
      throw new UnauthorizedException('没有权限访问该接口');
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 我们试一下:

这次用光光的账号登录:

访问 /aaa,会提示没有权限:

image.png

然后登录东东的账号:

然后访问 /aaa:

东东是有 query_aaa 的权限的,就可以正常访问了。

这样我们就通过 ACL 的方式完成了接口权限的控制。

但是不知道同学们有没有发现一个问题:

image.png

# 每次访问接口,都会触发这样 3 个表的关联查询。

效率太低了。

# 怎么优化一下呢?

有的同学说,登录的时候把权限也查出来放到 session 里不就行了么?

确实,可以在登录的时候做这件事情,把权限放到 session 里,之后就直接从 session 取就好了。

那还是延续现在的访问时查询权限的方案,怎么优化呢?

# 这时就需要 redis 了,redis 的缓存就是用来做这种优化的。

我们引入下 redis:

npm install redis
1

然后新建一个模块来封装 redis 操作:

nest g module redis
1

image.png

然后新建一个 service:

nest g service redis --no-spec
1

image.png

然后在 RedisModule 里添加 redis 的 provider:

import { Global, Module } from "@nestjs/common";
import { createClient } from "redis";
import { RedisService } from "./redis.service";

@Global()
@Module({
  providers: [
    RedisService,
    {
      provide: "REDIS_CLIENT",
      async useFactory() {
        const client = createClient({
          socket: {
            host: "localhost",
            port: 6379,
          },
        });
        await client.connect();
        return client;
      },
    },
  ],
  exports: [RedisService],
})
export class RedisModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

并使用 @Global 把这个模块声明为全局的。

这样,各个模块就都可以注入这个 RedisService 了。

然后在 RedisService 里添加一些 redis 操作方法:

import { Inject, Injectable } from '@nestjs/common';
import { RedisClientType } from 'redis';

@Injectable()
export class RedisService {

    @Inject('REDIS_CLIENT')
    private redisClient: RedisClientType

    async listGet(key: string) {
        return await this.redisClient.lRange(key, 0, -1);
    }

    async listSet(key: string, list: Array<string>, ttl?: number) {
        for(let i = 0; i < list.length;i++) {
            await this.redisClient.lPush(key, list[i]);
        }
        if(ttl) {
            await this.redisClient.expire(key, ttl);
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注入 redisClient,封装 listGet 和 listSet 方法,listSet 方法支持传入过期时间。

底层用的命令是 lrange 和 lpush、exprire。

然后在 PermissionGuard 里注入来用下:

image.png

# 先查询 redis、没有再查数据库并存到 redis,有的话就直接用 redis 的缓存结果。

key 为 user_${username}_permissions,这里的 username 是唯一的。

缓存过期时间为 30 分钟。

import { RedisService } from './../redis/redis.service';
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { UserService } from './user.service';

@Injectable()
export class PermissionGuard implements CanActivate {

  @Inject(UserService)
  private userService: UserService;

  @Inject(Reflector)
  private reflector: Reflector;

  @Inject(RedisService)
  private redisService: RedisService;

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

    const user = request.session.user;
    if(!user) {
      throw new UnauthorizedException('用户未登录');
    }

    let permissions = await this.redisService.listGet(`user_${user.username}_permissions`);

    if(permissions.length === 0) {
      const foundUser = await this.userService.findByUsername(user.username);
      permissions = foundUser.permissions.map(item => item.name);

      this.redisService.listSet(`user_${user.username}_permissions`, permissions, 60 * 30)
    }

    const permission = this.reflector.get('permission', context.getHandler());

    if(permissions.some(item => item === permission)) {
      return true;
    } else {
      throw new UnauthorizedException('没有权限访问该接口');
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

我们试一下:

# 这里如果你没跑 redis server,需要先通过 docker 把它跑起来。

具体怎么跑可以翻一下 redis 入门那节

image.png

然后先登录:

image.png

服务端打印了查询用户数据的 sql:

image.png

然后再访问 /aaa

image.png

又打印了 2 条关联查询的 sql:

image.png

我们去 RedisInsight 里看下:

image.png

可以看到这条缓存。

这时候你刷新多少次,都不会再产生 sql 了:

2023-06-22 17.16.43.gif

这时候查的就是 redis 缓存。

redis 是基于内存的,访问速度会比 mysql 快很多。这就是为什么要用 redis。

案例代码在小册仓库 (opens new window)。

# 总结

有的接口除了需要登录外,还需要权限。

只有登录用户有调用该接口的权限才能正常访问。

这节我们通过 ACL (Access Control List)的方式实现了权限控制,它的特点是用户直接和权限关联。

用户和权限是多对多关系,在数据库中会存在用户表、权限表、用户权限中间表。

登录的时候,把用户信息查出来,放到 session 或者 jwt 返回。

然后访问接口的时候,在 Guard 里判断是否登录,是否有权限,没有就返回 401,有的话才会继续处理请求。

我们采用的是访问接口的时候查询权限的方案,通过 handler 上用 SetMetadata 声明的所需权限的信息,和从数据库中查出来的当前用户的权限做对比,有相应权限才会放行。

但是这种方案查询数据库太频繁,需要用 redis 来做缓存。

当然,你选择登录的时候把权限一并查出来放到 session 或者 jwt 里也是可以的。

这就是通过 ACL 实现的权限控制。

编辑 (opens new window)
上次更新: 2025/9/2 10:26:16
MySQL + TypeORM + JWT 实现登录注册
基于 RBAC 实现权限控制

← MySQL + TypeORM + JWT 实现登录注册 基于 RBAC 实现权限控制→

最近更新
01
H5调用微信jssdk
09-28
02
VueVirtualScroller
09-19
03
IoC 解决了什么痛点问题?
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式