NestJS 集成 Redis 完整优化实现(验证码场景实战)
一、核心概述
本文基于 @nestjs-modules/ioredis 模块实现 NestJS 与 Redis 的无缝集成,以验证码生成、存储、验证为实战场景,提供标准化、可扩展的配置方案,同时优化了全局模块、环境变量配置、类型安全等核心细节,支持生产环境直接复用。
二、前期准备
2.1 安装核心依赖
bash
# Redis 核心依赖(@nestjs-modules/ioredis 基于 ioredis 封装,无需额外安装 ioredis)
npm install @nestjs-modules/ioredis
# 验证码生成依赖(本文实战场景使用)
npm install svg-captcha
# 类型提示(TypeScript 项目推荐)
npm install @types/svg-captcha --save-dev
2.2 启动 Redis 服务
-
本地环境:直接启动 Redis 服务(默认端口 6379)
bash# Linux/Mac redis-server # Windows(需先配置 Redis 环境变量) redis-server.exe -
生产环境:配置 Redis 服务地址、密码、端口后启动(后续配置中支持对应参数)
三、Redis 模块配置(全局化 + 环境变量优化)
3.1 环境变量配置(.env 文件)
在项目根目录创建 .env 文件,配置 Redis 连接信息,避免硬编码:
env
# Redis 配置
REDIS_URL=redis://localhost:6379 # 无密码连接格式
# 有密码格式:redis://:your_redis_password@localhost:6379/0(0 为数据库编号)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD= # 无密码留空
REDIS_DB=0 # Redis 数据库编号(默认 0)
REDIS_TTL=60 # 默认过期时间(秒,验证码场景推荐 60-300 秒)
3.2 全局 Redis 模块配置(推荐)
创建独立的 Redis 配置模块(src/db/redis/redis.module.ts),标记为全局模块,无需在各业务模块重复导入:
typescript
import { Global, Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-modules/ioredis';
import { ConfigService } from '@nestjs/config';
@Global() // 全局模块,所有业务模块可直接注入使用
@Module({
imports: [
// 异步配置:读取环境变量,支持生产环境动态切换配置
RedisModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
config: {
// 优先使用 URL 配置,简洁高效;也可单独配置 host/port/password
url: configService.get<string>('REDIS_URL'),
// 单独配置(备选方案,与 url 二选一即可)
// host: configService.get<string>('REDIS_HOST'),
// port: configService.get<number>('REDIS_PORT'),
// password: configService.get<string>('REDIS_PASSWORD'),
// db: configService.get<number>('REDIS_DB'),
retryStrategy: (times: number) => {
// 重连策略:失败后重试,避免单次连接失败导致服务异常
const delay = Math.min(times * 100, 3000);
return delay;
},
},
}),
}),
],
// 无需额外导出,全局模块的 RedisService 可直接注入
exports: [RedisModule],
})
export class GlobalRedisModule {}
3.3 根模块注册(app.module.ts)
在项目根模块中导入全局 Redis 模块,使其生效:
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GlobalRedisModule } from './db/redis/redis.module';
import { UserModule } from './service/user/user.module';
@Module({
imports: [
// 全局环境变量配置
ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
GlobalRedisModule, // 导入全局 Redis 模块
UserModule, // 业务模块(无需再导入 Redis 模块)
],
controllers: [],
providers: [],
})
export class AppModule {}
四、业务实现(验证码生成 + Redis 存储/验证)
4.1 用户服务(user.service.ts)
注入 RedisService,封装验证码生成、Redis 存储、Redis 读取核心逻辑,保证业务逻辑与数据操作分离:
typescript
import { Injectable } from '@nestjs/common';
import { RedisService } from '@nestjs-modules/ioredis';
import * as svgCaptcha from 'svg-captcha';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class UserService {
// 注入 Redis 服务和配置服务
constructor(
private readonly redisService: RedisService,
private readonly configService: ConfigService,
) {}
/**
* 生成 SVG 验证码
* @returns 验证码文本 + SVG 图片数据
*/
generateCaptcha() {
const captcha = svgCaptcha.create({
size: 4, // 验证码长度(4位数字/字母)
ignoreChars: '0o1iIl', // 忽略易混淆字符,提升用户体验
noise: 2, // 干扰线条数,增强安全性
color: true, // 开启字符彩色显示
background: '#f5f5f5', // 背景色,柔和不刺眼
width: 120, // 验证码图片宽度
height: 40, // 验证码图片高度
});
return {
text: captcha.text, // 验证码文本(用于验证)
data: captcha.data, // SVG 图片数据(用于返回给前端)
};
}
/**
* 存储验证码到 Redis
* @param key 存储键(推荐使用 IP + 唯一标识,避免同一 IP 重复覆盖)
* @param captchaText 验证码文本
* @param ttl 过期时间(秒,默认读取环境变量配置)
*/
async storeCaptcha(key: string, captchaText: string, ttl?: number) {
const expireTime = ttl || this.configService.get<number>('REDIS_TTL');
// 存储到 Redis:key - 验证码文本,EX - 过期时间单位(秒)
await this.redisService.set(key, captchaText.toLowerCase(), 'EX', expireTime);
}
/**
* 从 Redis 获取验证码
* @param key 存储键
* @returns 验证码文本(不存在返回 null)
*/
async getCaptcha(key: string): Promise<string | null> {
return await this.redisService.get(key);
}
/**
* 验证验证码有效性
* @param key 存储键
* @param inputCaptcha 用户输入的验证码
* @returns 验证结果
*/
async validateCaptcha(key: string, inputCaptcha: string): Promise<boolean> {
if (!inputCaptcha) return false;
// 获取 Redis 中存储的验证码
const storedCaptcha = await this.getCaptcha(key);
if (!storedCaptcha) return false;
// 忽略大小写对比(提升用户体验)
return inputCaptcha.toLowerCase() === storedCaptcha;
}
}
4.2 用户控制器(user.controller.ts)
实现验证码获取接口和验证接口,处理 HTTP 请求与响应,复用 UserService 中的逻辑:
typescript
import { Controller, Get, Post, Body, Res, Ip, HttpCode, HttpStatus } from '@nestjs/common';
import { UserService } from './user.service';
import { Response } from 'express';
import { v4 as uuidv4 } from 'uuid'; // 可选:生成唯一标识,避免同一 IP 重复覆盖
@Controller({
path: 'user',
version: '1', // 接口版本 v1
})
export class UserController {
constructor(private readonly userService: UserService) {}
/**
* 获取 SVG 验证码图片
* @param res Express 响应对象
* @param ip 客户端 IP 地址
* @returns SVG 图片
*/
@Get('get/code')
async getCaptchaImage(@Res() res: Response, @Ip() ip: string) {
// 1. 生成验证码
const captcha = this.userService.generateCaptcha();
// 2. 构建 Redis 存储键(IP + UUID,避免同一 IP 多次请求覆盖验证码)
const redisKey = `captcha:${ip}:${uuidv4()}`;
// 可选:将 redisKey 返回给前端,验证时需携带该 key
// res.cookie('captchaKey', redisKey, { maxAge: 5 * 60 * 1000, httpOnly: true });
// 3. 存储验证码到 Redis(简化版:直接使用 IP 作为 key,适合简单场景)
const simpleRedisKey = `captcha:${ip}`;
await this.userService.storeCaptcha(simpleRedisKey, captcha.text);
// 4. 返回 SVG 图片
res.type('image/svg+xml'); // 明确响应类型为 SVG
res.status(HttpStatus.OK).send(captcha.data);
}
/**
* 验证验证码有效性
* @param inputCaptcha 用户输入的验证码
* @param ip 客户端 IP 地址
* @returns 验证结果
*/
@Post('validate/captcha')
@HttpCode(HttpStatus.OK) // 指定响应状态码 200
async validateCaptcha(
@Body('captcha') inputCaptcha: string,
@Ip() ip: string,
) {
// 构建 Redis 键(与存储时保持一致)
const simpleRedisKey = `captcha:${ip}`;
// 验证验证码
const isValid = await this.userService.validateCaptcha(simpleRedisKey, inputCaptcha);
if (!isValid) {
return {
success: false,
code: 400,
message: '验证码错误或已过期',
data: null,
};
}
// 验证成功后,删除 Redis 中的验证码(避免重复使用)
await this.userService.redisService.del(simpleRedisKey);
return {
success: true,
code: 200,
message: '验证码验证通过',
data: null,
};
}
}
4.3 用户模块配置(user.module.ts)
无需额外导入 Redis 模块(全局模块已生效),仅需注册控制器和服务:
typescript
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
五、接口测试
5.1 测试准备
-
启动 Redis 服务(默认端口 6379)
-
启动 NestJS 应用
bashnpm run start:dev -
接口前缀:若配置了全局前缀
/api,接口地址需添加/api
5.2 接口信息
| 接口功能 | 请求方法 | 接口地址 | 请求参数 | 响应说明 |
|---|---|---|---|---|
| 获取验证码图片 | GET | /api/v1/user/get/code | 无(自动获取客户端 IP) | SVG 格式验证码图片 |
| 验证验证码 | POST | /api/v1/user/validate/captcha | { "captcha": "XXXX" } | JSON 格式验证结果 |
5.3 响应示例
验证码验证成功
json
{
"success": true,
"code": 200,
"message": "验证码验证通过",
"data": null
}
验证码验证失败
json
{
"success": false,
"code": 400,
"message": "验证码错误或已过期",
"data": null
}
六、核心优化点说明
- 全局模块化 :Redis 模块标记为
@Global(),所有业务模块无需重复导入,简化配置 - 环境变量配置 :通过
.env文件管理 Redis 连接信息,支持开发/生产环境动态切换,避免硬编码 - 类型安全:使用 TypeScript 强类型约束,注入服务和配置时类型明确,减少运行时错误
- 高可用优化:配置 Redis 重连策略,避免单次连接失败导致服务异常
- 用户体验优化 :
- 忽略易混淆字符(0o1iIl),减少用户输入错误
- 验证码对比忽略大小写,提升使用便捷性
- 验证成功后删除 Redis 验证码,避免重复使用
- 安全性优化 :
- 使用
IP + UUID作为 Redis 键(可选),避免同一 IP 重复覆盖验证码 - 验证码设置过期时间,防止恶意攻击
- 使用
- 可扩展性 :封装的
storeCaptcha、getCaptcha方法可复用于其他需要 Redis 存储的场景(如 token 缓存、用户信息缓存等)
七、生产环境扩展建议
-
Redis 集群配置 :若业务为分布式部署,可配置 Redis 集群,提高可用性和性能
typescriptRedisModule.forRootAsync({ inject: [ConfigService], useFactory: (configService: ConfigService) => ({ config: [ { url: 'redis://localhost:6379' }, { url: 'redis://localhost:6380' }, { url: 'redis://localhost:6381' }, ], }), }), -
密码加密:Redis 若配置密码,生产环境需通过环境变量安全存储,避免明文暴露
-
键名规范 :统一 Redis 键名前缀(如
captcha:、token:),便于后续管理和排查 -
连接池优化 :配置 Redis 连接池参数,提升高并发场景下的性能
typescriptconfig: { url: 'redis://localhost:6379', maxRetriesPerRequest: 3, // 单次请求最大重试次数 connectTimeout: 5000, // 连接超时时间(毫秒) } -
日志监控:添加 Redis 操作日志,便于排查生产环境中的数据存储/读取异常
八、总结
本文通过 @nestjs-modules/ioredis 实现了 NestJS 与 Redis 的标准化集成,以验证码场景为实战案例,覆盖了配置、存储、读取、验证全流程,同时优化了全局模块、环境变量、高可用等核心细节。该方案可直接复用于生产环境,也可扩展到 token 缓存、热点数据缓存、分布式锁等其他 Redis 应用场景。