Nestjs如何接入Redis
笔记
Redis 是一个开源(BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。我们可以使用 redis 来稍微的弥补 jwt 策略的缺陷
- Redis 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。
- Redis 使用单进程单线程模型的(K,V)数据库,将数据存储在内存中,存取均不会受到硬盘 IO 的限制,因此其执行速度极快。另外单线程也能处理高并发请求,还可以避免频繁上下文切换和锁的竞争,如果想要多核运行也可以启动多个实例。
- 数据结构简单,对数据操作也简单,Redis 不使用表,不会强制用户对各个关系进行关联,不会有复杂的关系限制,其存储结构就是键值对,类似于 HashMap,HashMap 最大的优点就是存取的时间复杂度为 O(1)。
- Redis 使用多路 I/O 复用模型,为非阻塞 IO。
# 本地安装
百度操作下载好 redis。本地 redis 安装后如何跑: 到 redis 根目录,打开 cmd : redis-server.exe redis.windows.conf。 执行完 cmd 之后,可以使用 Redis Desktop Manager 可视化工具来查看本地 redis 情况。破解版去网上下载
# nestjs 接入 redis
# 安装依赖
- pnpm add --save @liaoliaots/nestjs-redis ioredis ,文档 (opens new window), 注意!千万不要直接下载 nestjs-redis 依赖,它会去下载 skunight 这个哥们的包,有 BUG,这上海的老哥已经快一年没更新了,外国佬都快杀疯了,踩过坑了别人别踩-。-
- 找到 libs/db/src/db.module.ts 接入 redis
import { RedisModule } from '@liaoliaots/nestjs-redis';
imports: [
RedisModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
config: {
name: configService.get('REDIS_NAME', 'test-redis'),
host: configService.get('REDIS_HOST', '127.0.0.1'),
port: configService.get('REDIS_PORT', 6379),
password: configService.get('REDIS_PASSWORD', 'cd@redis'),
},
}),
}),
],
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建 service 服务
- nest g s --no-spec , 选择 admin , 名字为 redis
- 找到 redis.service 文件夹
import { RedisService } from '@liaoliaots/nestjs-redis';
import { Injectable } from '@nestjs/common';
import { Redis } from 'ioredis';
@Injectable()
// (这里的命名和redisService冲突了,我将它改成了RedisCacheService)
export class RedisCacheService {
public client: Redis;
constructor(private redisService: RedisService) {
this.createRedis();
}
// 创建redis服务
async createRedis() {
this.client = await this.redisService.getClient();
}
// 获取redis缓存数据
async get(key: string): Promise<any> {
if (!this.client) {
await this.createRedis();
}
const data = await this.client.get(key);
if (data) return JSON.parse(data);
return null;
}
// 设置redis缓存
async set(key: string, value: any, seconds?: number): Promise<any> {
value = JSON.stringify(value);
if (!this.client) {
await this.createRedis();
}
if (!seconds) {
await this.client.set(key, value);
} else {
await this.client.set(key, value, 'EX', seconds);
}
}
// 根据key删除redis缓存数据
async del(key: string): Promise<any> {
return await this.client.del(key);
}
// 获取所有redis缓存
async flushall(): Promise<any> {
return await this.client.flushall();
}
}
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
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
# 实现单点登录
实现单点登录的逻辑就是,当我们在为用户 sign token 的时候,我们通过 key: 用户 id, value:token 的形式存储进 redis,如果下次还有同样的用户 id 登录就把原来的 redis 中的 key 对应的值换成新的,这样先前的那个用户 再次访问的时候发现这个 token 和之前的不相同,那么就认为它在别的地方登录了,本地就强制下线, 从而保证一个用户只允许在一个地方登录,这里暂不考虑 redis 缓存丢失,服务器宕机等问题。
- 我统一把登录相关的鉴权接口写在 auth.service 里,我们来到 auth.service
// auth.module 导入redis服务
import { RedisCacheService } from '../redis/redis.service';
providers: [
.....
RedisCacheService,
],
// auth.service 注入服务并编写接口
constructor(
@Inject(RedisCacheService)
private readonly redisCacheService: RedisCacheService,
) {}
async createRedisByToken({ name, id, token }): Promise<any> {
this.redisCacheService.set(
`jwt-${id}-${name}`,
token,
Number(this.configService.get('REDIS_CACHE_TIME', 60 * 60)),
);
}
async getRedisByToken({ name, id }): Promise<string> {
return this.redisCacheService.get(`jwt-${id}-${name}`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
来到 user.controller,我们在成功登录后使用 createRedisByToken 来为用户生成缓存
// user.controller
...
const token = await this.authService.creatToken({
name,
id,
});
await this.authService.createRedisByToken({
name,
id,
token: token.accessToken,
});
...
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
这个时候我们就拥有了用户缓存,在来到 jwt.strategy 下面为该策略书写 redis 验证
// jwt.strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
// 这里我们需要增加passReqToCallback:true参数,让validate返回req从而获取前端传入的token
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
ignoreExpiration: false,
passReqToCallback: true,
} as StrategyOptions);
}
async validate(req, payload: JwtPayloadToken, done: any) {
const originToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
const { name, id } = payload;
const cacheToken = await this.authService.getRedisByToken({ name, id });
const user = await this.authService.validateUser(name);
console.log({ originToken, cacheToken });
//单点登陆验证, 要排除redis为空的情况
if (cacheToken && cacheToken !== originToken) {
throw new ApiException(
'您账户已经在另一处登陆,请重新登陆',
400,
ApiCodeEnum.USER_LOGGED,
);
}
if (!user || user.id !== Number(id)) {
return done(
new ApiException('token无效', 400, ApiCodeEnum.TOKEN_OVERDUE),
false,
);
}
done(null, user);
}
}
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
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
到此为止,登录鉴权就完成了。可以自己在 nest 中构造 demo 实践一下,实践才能得真知。
编辑 (opens new window)
上次更新: 2023/03/27, 22:09:34