• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Nest 通关秘籍

    • 1.开篇词
    • 2.给你 5 个学习 Nest 的理由,你会心动么
    • 3.Nest 基础概念扫盲
    • 4.快速掌握 Nest CLI
    • 5.五种HTTP数据传输方式
    • 6.IoC 解决了什么痛点问题?
    • 7.如何调试 Nest 项目
    • 8.使用多种 Provider,灵活注入对象
    • 9.全局模块和生命周期
    • 10.AOP 架构有什么好处?
    • 11.一网打尽 Nest 全部装饰器
    • 12.Nest 如何自定义装饰器
    • 13.Metadata 和 Reflector
    • 14.ExecutionContext:切换不同上下文
    • 15.Module 和 Provider 的循环依赖怎么处理?
    • 16.如何创建动态模块
    • 17.Nest 和 Express 的关系,如何切到 fastify
    • 18.Nest 的 Middleware
    • 19.RxJS 和 Interceptor
    • 20.内置 Pipe 和自定义 Pipe
    • 21.如何使用 ValidationPipe 验证 post 请求参数
    • 22.如何自定义 Exception Filter
    • 23.图解串一串 Nest 核心概念
    • 24.接口如何实现多版本共存
    • 25.Express 如何使用 multer 实现文件上传
    • 26.Nest 如何使用 multer 实现文件上传
    • 27.图书管理系统:需求分析和原型图
    • 28.图书管理系统:用户模块后端开发
    • 29.图书管理系统:图书模块后端开发
    • 30.图书管理系统:用户模块前端开发
    • 31.图书管理系统:图书模块前端开发--图书搜索
    • 32.图书管理系统:图书模块前端开发--图书增删改
    • 33.图书管理系统:项目总结
    • 34.大文件分片上传
    • 35.最完美的 OSS 上传方案
    • 36.Nest 里如何打印日志?
    • 37.为什么 Node 里要用 Winston 打印日志?
    • 38.Nest 集成日志框架 Winston
    • 39.通过 Desktop 学 Docker 也太简单了
    • 40.你的第一个 Dockerfile
    • 41.Nest 项目如何编写 Dockerfile
    • 42.提升 Dockerfile 水平的 5 个技巧
    • 43.Docker 是怎么实现的?
    • 44.为什么 Node 应用要用 PM2 来跑?
    • 45.快速入门 MySQL
    • 46.SQL 查询语句的所有语法和函数
    • 47.一对一、join 查询、级联方式
    • 48.一对多、多对多关系的表设计
    • 49.子查询和 EXISTS
    • 50.SQL 综合练习
    • 51.MySQL 的事务和隔离级别
    • 52.MySQL 的视图、存储过程和函数
    • 53.使用 Node 操作 MySQL 的两种方式
    • 54.快速掌握 TypeORM
    • 55.TypeORM 一对一的映射和关联 CRUD
    • 56.TypeORM 一对多的映射和关联 CRUD
    • 57.TypeORM 多对多的映射和关联 CRUD
    • 58.在 Nest 里集成 TypeORM
    • 59.TypeORM 如何保存任意层级的关系?
    • 60.为什么生产环境要用 TypeORM 的 migration 迁移功能?
    • 61.Nest 项目里如何使用 TypeORM 迁移
    • 62.如何动态读取不同环境的配置?
    • 63.快速入门 Redis
    • 64.在 Nest 里操作 Redis
    • 65.为什么不用 cache-manager 操作 Redis?
    • 66.两种登录状态保存方式:JWT、Session
    • 67.Nest 里实现 Session 和 JWT
    • 68.MySQL + TypeORM + JWT 实现登录注册
    • 69.基于 ACL 实现权限控制
    • 70.基于 RBAC 实现权限控制
    • 71.基于 access_token 和 refresh_token 实现登录状态无感刷新
    • 72.单 token 无限续期,实现登录状态无感刷新
    • 73.使用 passport 做身份认证
    • 74.passport 实现 GitHub 三方账号登录
    • 75.passport 实现 Google 三方账号登录
    • 76.为什么要使用 Docker Compose ?
    • 77.Docker 容器通信的最简单方式:桥接网络
    • 78.Docker 支持重启策略,是否还需要 PM2
    • 79.快速掌握 Nginx 的 2 大核心用法
    • 80.基于 Nginx 实现灰度系统
    • 81.基于 Redis 实现分布式 session
    • 82.Redis + 高德地图,实现附近的充电宝
    • 83.用 Swagger 自动生成 api 文档
    • 84.如何灵活创建 DTO
    • 85.class-validator 的内置装饰器,如何自定义装饰器
    • 86.序列化 Entity,你不需要 VO 对象
    • 87.手写序列化 Entity 的拦截器
    • 88.使用 compodoc 生成文档
    • 89.Node 如何发邮件?
    • 90.实现基于邮箱验证码的登录
    • 91.定时任务 + Redis 实现阅读量计数
    • 92.Nest 的 3 种定时任务
    • 93.Nest 里如何实现事件通信?
    • 94.HttpModule + pinyin 实现天气预报查询服务
    • 95.如何记录请求日志
    • 96.短链服务?自己写一个
    • 97.Nest 实现 Server Sent Event 数据推送
    • 98.用 minio 自己搭一个 OSS 服务
    • 99.前端如何直传文件到 Minio
    • 100.基于 sharp 实现 gif 压缩工具
    • 101.大文件如何实现流式下载?
    • 102.Puppeteer 实现爬虫,爬取 BOSS 直聘全部前端岗位
    • 103.实现扫二维码登录
    • 104.Nest 的 REPL 模式
    • 105.实现 Excel 导入导出
    • 106.如何用代码动态生成 PPT
    • 107.如何拿到服务器 CPU、内存、磁盘状态
    • 108.Nest 如何实现国际化?
    • 109.会议室预订系统:需求分析和原型图
    • 110.会议室预订系统:技术方案和数据库设计
    • 111.会议室预订系统:用户管理模块-用户注册
    • 112.会议室预订系统:用户管理模块-配置抽离、登录认证鉴权
    • 113.会议室预订系统:用户管理模块-interceptor、修改信息接口
    • 114.会议室预订系统:用户管理模块-用户列表和分页查询
    • 115.会议室预订系统:用户管理模块-swagger 接口文档
    • 116.会议室预订系统:用户管理模块-用户端登录注册页面
    • 117.会议室预订系统:用户管理模块-用户端信息修改页面
    • 118.会议室预订系统:用户管理模块-头像上传
    • 119.会议室预订系统:用户管理模块-管理端用户列表页面
    • 120.会议室预订系统:用户管理模块-管理端信息修改页面
    • 121.会议室预订系统:会议室管理模块-后端开发
    • 122.会议室预订系统:会议室管理模块-管理端前端开发
    • 123.会议室预订系统:会议室管理模块-用户端前端开发
    • 124.会议室预订系统:预定管理模块-后端开发
    • 125.会议室预订系统:预定管理模块-管理端前端开发
    • 126.会议室预订系统:预定管理模块-用户端前端开发
    • 127.会议室预订系统:统计管理模块-后端开发
    • 128.会议室预订系统:统计管理模块-前端开发
    • 129.会议室预订系统:后端项目部署到阿里云
    • 130.会议室预订系统:前端项目部署到阿里云
    • 131.会议室预定系统:用 migration 初始化表和数据
    • 132.会议室预定系统:文件上传 OSS
    • 133.会议室预定系统:Google 账号登录后端开发
    • 134.会议室预定系统:Google 账号登录前端开发
    • 135.会议室预定系统:后端代码优化
    • 136.会议室预定系统:集成日志框架 winston
    • 137.会议室预定系统:前端代码优化
    • 138.会议室预定系统:全部功能测试
    • 139.会议室预定系统:项目总结
    • 140.Nest 如何创建微服务?
    • 141.Nest 的 Monorepo 和 Library
    • 142.用 Etcd 实现微服务配置中心和注册中心
    • 143.Nest 集成 Etcd 做注册中心、配置中心
    • 144.用 Nacos 实现微服务配置中心和注册中心
    • 145.基于 gRPC 实现跨语言的微服务通信
    • 146.快速入门 ORM 框架 Prisma
    • 147.Prisma 的全部命令
    • 148.Prisma 的全部 schema 语法
    • 149.Primsa Client 单表 CRUD 的全部 api
    • 150.Prisma Client 多表 CRUD 的全部 api
    • 151.在 Nest 里集成 Prisma
    • 152.为什么前端监控系统要用 RabbitMQ?
    • 153.基于 Redis 实现关注关系
    • 154.基于 Redis 实现各种排行榜(周榜、月榜、年榜)
    • 155.考试系统:需求分析
    • 156.考试系统:技术方案和数据库设计
    • 157.考试系统:微服务、Lib 拆分
    • 158.考试系统;用户注册
    • 159.考试系统:用户登录、修改密码
    • 160.考试系统:考试微服务
    • 161.考试系统:登录、注册页面
    • 162.考试系统:修改密码、试卷列表页面
    • 163.考试系统:新增试卷、回收站
    • 164.考试系统:试卷编辑器
    • 165.考试系统:试卷回显、预览、保存
    • 166.考试系统:答卷微服务
    • 167.考试系统:答题页面
    • 168.考试系统:自动判卷
    • 169.考试系统:分析微服务、排行榜页面
    • 170.考试系统:整体测试
    • 171.考试系统:项目总结
    • 172.用 Node.js 手写 WebSocket 协议
    • 173.Nest 开发 WebSocket 服务
    • 174.基于 Socket.io 的 room 实现群聊
    • 175.聊天室:需求分析和原型图
    • 176.聊天室:技术选型和数据库设计
    • 177.聊天室:用户注册
    • 178.聊天室:用户登录
    • 179.聊天室:修改密码、修改信息
    • 180.聊天室:好友列表、发送好友申请
    • 181.聊天室:创建聊天室、加入群聊
    • 182.聊天室:登录、注册页面开发
    • 183.聊天室:修改密码、信息页面开发
    • 184.聊天室:头像上传
    • 185.聊天室:好友∕群聊列表页面
    • 186.聊天室:添加好友弹窗、通知页面
    • 187.聊天室:聊天功能后端开发
    • 188.聊天室:聊天功能前端开发
    • 189.聊天室:一对一聊天
    • 190.聊天室:创建群聊、进入群聊
    • 191.聊天室:发送表情、图片、文件
    • 192.聊天室:收藏
    • 193.聊天室:全部功能测试
    • 194.聊天室:项目总结
    • 195.MongoDB 快速入门
    • 196.使用 mongoose 操作 MongoDB 数据库
    • 197.GraphQL 快速入门
    • 198.Nest 开发 GraphQL 服务:实现 CRUD
    • 199.GraphQL + Primsa + React 实现 TodoList
    • 200.如何调试 Nest 源码?

前面我们学习了登录鉴权的两种方式 session 和 jwt。

session 是在服务端保存用户数据,然后通过 cookie 返回 sessionId。cookie 在每次请求的时候会自动带上,服务端就能根据 sessionId 找到对应的 session,拿到用户的数据

而 jwt 是把所有的用户数据保存在加密后的 token 里返回,客户端只要在 authorization 的 header 里带上 token,服务端就能从中解析出用户数据。

jwt 天然是支持分布式的,比如有两个服务器的时候,任何一个服务器都能从 token 出拿到用户数据:

但是 session 的方式不行,它的数据是存在单台服务器的内存的,如果再请求另一台服务器就找不到对应的 session 了:

那如何让 session 支持分布式环境呢?

一种方式就是做 session 的同步,在多台服务器之间复制 session。

另一种方式就是自己基于 redis 实现一个分布式 session 了。

这节我们就来实现一下。

首先我们来分析下思路:

分布式 session 就是在多台服务器都可以访问到同一个 session。

我们可以在 redis 里存储它:

用户第一次请求的时候,生成一个随机 id,以它作为 key,存储的对象作为 value 放到 redis 里。

之后携带 cookie 的时候,根据其中的 sid 来取 redis 中的值,注入 handler。

修改 session 之后再设置到 redis 里。

这样就完成了 session 的创建、保存、修改。

我们具体实现下:

nest new redis-session-test -p npm

创建 nest 项目。

安装 redis 的包:

npm install --save redis

然后创建个 redis 模块:

nest g module redis
nest g service redis

在 RedisModule 创建连接 redis 的 provider,导出 RedisService,并把这个模块标记为 @Global 模块

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 {}

然后在 RedisService 里注入 REDIS_CLIENT,并封装一些方法:

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

@Injectable()
export class RedisService {
    @Inject("REDIS_CLIENT")
    private redisClient: RedisClientType;

    async hashGet(key: string) {
        return await this.redisClient.hGetAll(key);
    }

    async hashSet(key: string, obj: Record<string, any>, ttl?: number) {
        for (let name in obj) {
            await this.redisClient.hSet(key, name, obj[name]);
        }

        if (ttl) {
            await this.redisClient.expire(key, ttl);
        }
    }
}

因为我们要操作的是对象结构,比较适合使用 hash。

redis 的 hash 有这些方法:

  • HSET key field value: 设置指定哈希表 key 中字段 field 的值为 value。
  • HGET key field:获取指定哈希表 key 中字段 field 的值。
  • HMSET key field1 value1 field2 value2 ...:同时设置多个字段的值到哈希表 key 中。
  • HMGET key field1 field2 ...:同时获取多个字段的值从哈希表 key 中。
  • HGETALL key:获取哈希表 key 中所有字段和值。
  • HDEL key field1 field2 ...:删除哈希表 key 中一个或多个字段。
  • HEXISTS key field:检查哈希表 key 中是否存在字段 field。
  • HKEYS key:获取哈希表 key 中的所有字段。
  • HVALUES key:获取哈希表 key 中所有的值。-HLEN key:获取哈希表 key 中字段的数量。
  • HINCRBY key field increment:将哈希表 key 中字段 field 的值增加 increment。
  • HSETNX key field value:只在字段 field 不存在时,设置其值为 value。

这里我们就用到 hGetAll 和 hSet 方法,再就是用 expire 设置 key 的过期时间。

这里的 Record<string, any> 是对象类型的意思。

然后再封装个 SessionModule:

nest g module session
nest g service session --no-spec

导出 SessionService,并且设置 SessionModule 为 Global:

import { Global, Module } from "@nestjs/common";
import { SessionService } from "./session.service";

@Global()
@Module({
    providers: [SessionService],
    exports: [SessionService],
})
export class SessionModule {}

然后实现 SessionService:

import { Inject, Injectable } from "@nestjs/common";
import { RedisService } from "src/redis/redis.service";

@Injectable()
export class SessionService {
    @Inject(RedisService)
    private redisService: RedisService;

    async setSession(
        sid: string,
        value: Record<string, any>,
        ttl: number = 30 * 60
    ) {
        if (!sid) {
            sid = this.generateSid();
        }
        await this.redisService.hashSet(`sid_${sid}`, value, ttl);
        return sid;
    }

    async getSession(sid: string) {
        return await this.redisService.hashGet(`sid_${sid}`);
    }

    generateSid() {
        return Math.random().toString().slice(2, 12);
    }
}

setSession 就是用 sid_xx 的 key 在 redis 里创建 string 的数据结构。

getSession 是用 sid_xx 从 redis 取值。

generateSid 是生成随机的 session id

setSession 的时候如果没有传入 sid,则随机生成一个,并返回 sid。

我们在 AppController 添加个方法测试下:

@Inject(SessionService)
private sessionService: SessionService;

@Get('count')
async count(@Req() req: Request, @Res() res: Response) {
    const sid = req.cookies?.sid;

    const session = await this.sessionService.getSession(sid);

}

这里用到 cookie,需要安装 cookie-parser 的包:

npm install --save cookie-parser

在 main.ts 里启用:

现在 getSession 返回的是 Record<string, any> 也就是对象类型,但并不知道有啥具体的属性。

所以我们改造下 getSession 的类型声明加个重载:

async getSession<SessionType extends Record<string,any>>(sid: string): Promise<SessionType>;
async getSession(sid: string) {
    return await this.redisService.hashGet(`sid_${sid}`);
}

这样再用的时候,当不传类型参数,返回的是默认类型 Record<string, any>:

传入类型参数之后,返回的就是该类型了:

为什么这里是 string 呢?

因为 redis 虽然可以存整数、浮点数,但是它会转为 string 来存,所以取到的是 string,需要自己转换一下。

我们实现下计数逻辑:

@Inject(SessionService)
private sessionService: SessionService;

@Get('count')
async count(@Req() req: Request, @Res({ passthrough: true}) res: Response) {
    const sid = req.cookies?.sid;

    const session = await this.sessionService.getSession<{count: string}>(sid);

    const curCount = session.count ? parseInt(session.count) + 1 : 1;
    const curSid = await this.sessionService.setSession(sid, {
      count: curCount
    });

    res.cookie('sid', curSid, { maxAge: 1800000 });
    return curCount;
}

先根据 cookie 的 sid 调用 getSession 取 session。

拿到的如果有 count,就 + 1 之后放回去,没有就设置 1

然后 setSession 更新 session。

在 cookie 中返回 sid。

默认用了 @Res 传入 response 之后就需要手动返回响应了,比如 res.end('xxx'),如果还是想让 nest 把返回值作为响应,就加个 passthrough: true。

我们测试下:

我们自己实现的 session 就生效了:

在 Redis Insight 里可以看到 session 的值

而且这个 session 是支持分布式的。

我们用 nginx 做网关层,使用轮询的负载均衡策略,那请求可能到任何一台服务器上。

如果是之前的 session,当前机器没有对应的 session 对象,就拿不到登录状态。

而现在基于 redis 存储的 session,不管请求到了哪台服务器,都能从 redis 中取出对应的 session 从而拿到登录状态、用户数据。

这就是分布式 session。

案例代码在小册仓库。

总结

session 是在服务端内存存储会话数据,通过 cookie 中的 session id 关联。

但它不支持分布式,换台机器就不行了。

jwt 是在客户端存储会话数据,所以天然支持分布式。

我们通过 redis 自己实现了分布式的 session。

我们使用的是 hash 的数据结构,封装了 RedisModule 来操作 Redis。

又封装了 SessionModule 来读写 redis 中的 session,以 sid_xxx 为 key。

之后在 ctronller 里就可以读取和设置 session 了,用起来和内置的传统 session 差不多。但是它是支持分布式的。

如果你想在分布式场景下用 session,就自己基于 redis 实现一个吧。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
80.基于 Nginx 实现灰度系统
Next
82.Redis + 高德地图,实现附近的充电宝