NextJs14 中中间件 Middware 与 redis,jwt 的报错

时之世 发布于 17 天前 367 次阅读 预计阅读时间: 4 分钟 最后更新于 17 天前 969 字 1 评论


AI 摘要

在 NextJs14 中,当使用 ioredis 库时,可能会遇到报错 TypeError: Cannot read properties of undefined (reading 'charCodeAt') 。这是因为 NextJs 的中间件在边缘运行 (Edge) 时无法使用.env 获取环境变量。所以需要在 RedisClient 类中进行相应的处理,以确保正确连接到 Redis 数据库。另外,在使用 jsonwebtoken 库时,会遇到错误 Error: Invalid token: The edge runtime does not support Node.js 'crypto' module,也是由于边缘运行环境下缺少 Node.js 的'crypto' 模块导致的。因此,在 JwtService 类中需要注意这一点,以避免无法找到'crypto' 模块而出现问题。

先说明使用的库:

{
    "ioredis": "^5.6.0",
    "jsonwebtoken": "^9.0.2"
}

ioredis

报错显示为 TypeError: Cannot read properties of undefined (reading 'charCodeAt'),是因为 NextJs 的中间件是在边缘运行 (Edge), 没有不能使用.env 获取环境变量

// utils/redis.ts
import Redis from 'ioredis'

class RedisClient {
    private static instance: RedisClient
    private redisClient: Redis

    // 当前使用的数据库索引
    private currentDb: number = 0

    private constructor() {
        // 初始化 Redis 客户端
        this.redisClient = new Redis({
            host: process.env.REDIS_HOST || '127.0.0.1',
            port: parseInt(process.env.REDIS_PORT || '6379'),
            password: process.env.REDIS_PASSWORD || '123456',
            db: parseInt(process.env.REDIS_DB || '0'), // 默认使用配置文件中的 DB
            connectTimeout: parseInt(
                process.env.REDIS_CONNECT_TIMEOUT || '3000'
            ),
            retryStrategy(times) {
                // 当连接失败时的重试策略
                return Math.min(times * 50, 2000)
            }
        })

        // 监听连接错误
        this.redisClient.on('error', (error) => {
            console.error('Redis connection error:', error)
        })

        // 监听连接成功
        this.redisClient.on('connect', () => {
            console.log(`Connected to Redis (DB ${this.currentDb})`)
        })
    }

    // 单例模式:确保只有一个 Redis 实例
    public static getInstance(): RedisClient {
        if (!RedisClient.instance) {
            RedisClient.instance = new RedisClient()
        }
        return RedisClient.instance
    }

    // 切换数据库
    public async selectDb(dbIndex: number): Promise<void> {
        try {
            await this.redisClient.select(dbIndex)
            this.currentDb = dbIndex
            console.log(`Switched to Redis DB ${dbIndex}`)
        } catch (error) {
            console.error('Error switching Redis DB:', error)
            throw error
        }
    }

    // 设置键值对(支持设置过期时间)
    public async set(key: string, value: any, ttl?: number): Promise<void> {
        try {
            const serializedValue = this.serialize(value)
            if (ttl) {
                await this.redisClient.set(key, serializedValue, 'EX', ttl) // 设置过期时间(秒)
            } else {
                await this.redisClient.set(key, serializedValue)
            }
        } catch (error) {
            console.error('Redis set error:', error)
            throw error
        }
    }

    // 获取键的值并反序列化
    public async get<T = any>(key: string): Promise<T | null> {
        try {
            const value = await this.redisClient.get(key)
            return this.deserialize(value)
        } catch (error) {
            console.error('Redis get error:', error)
            throw error
        }
    }

    // 删除键
    public async del(key: string): Promise<void> {
        try {
            await this.redisClient.del(key)
        } catch (error) {
            console.error('Redis del error:', error)
            throw error
        }
    }

    // 检查键是否存在
    public async exists(key: string): Promise<boolean> {
        try {
            const result = await this.redisClient.exists(key)
            return result === 1
        } catch (error) {
            console.error('Redis exists error:', error)
            throw error
        }
    }

    // 设置哈希字段(支持复杂对象)
    public async hset(key: string, field: string, value: any): Promise<void> {
        try {
            const serializedValue = this.serialize(value)
            await this.redisClient.hset(key, field, serializedValue)
        } catch (error) {
            console.error('Redis hset error:', error)
            throw error
        }
    }

    // 获取哈希字段的值并反序列化
    public async hget<T = any>(key: string, field: string): Promise<T | null> {
        try {
            const value = await this.redisClient.hget(key, field)
            return this.deserialize(value)
        } catch (error) {
            console.error('Redis hget error:', error)
            throw error
        }
    }

    // 删除 Redis 客户端连接
    public async quit(): Promise<void> {
        try {
            await this.redisClient.quit()
            console.log('Redis client disconnected')
        } catch (error) {
            console.error('Redis quit error:', error)
            throw error
        }
    }

    // 将值序列化为 JSON 字符串
    private serialize(value: any): string {
        return JSON.stringify(value)
    }

    // 将 JSON 字符串反序列化为原始值
    private deserialize(value: string | null): any {
        if (!value) return null
        try {
            return JSON.parse(value)
        } catch (error) {
            console.error('Error deserializing JSON:', error)
            return value // 返回原始字符串以防解析失败
        }
    }
}

export const RedisClientInstance = RedisClient.getInstance()

jsonwebtoken

报错显示为 Error: Invalid token: The edge runtime does not support Node.js 'crypto' module. 同样是由于边缘运行,没有 node 的环境,导致找不到 crypto

import type { JwtPayload, SignOptions } from 'jsonwebtoken'
import jwt from 'jsonwebtoken'

/**
 * JWT 工具类
 */
class JwtService {
    private secretKey: string

    /**
     * 构造函数
     * @param secretKey - 用于签名和验证的密钥
     */
    constructor(secretKey: string) {
        this.secretKey = secretKey
    }

    /**
     * 生成 JWT Token
     * @param payload - 要加密的数据(可以是对象或字符串)
     * @param options - 签名选项(可选)
     * @returns 生成的 JWT Token
     */
    public generateToken(
        payload: string | object,
        options?: SignOptions
    ): string {
        try {
            return jwt.sign(payload, this.secretKey, options)
        } catch (error: any) {
            throw new Error(`Failed to generate token: ${error.message}`)
        }
    }

    /**
     * 验证并解析 JWT Token
     * @param token - 要验证的 JWT Token
     * @returns 解析后的数据(payload)
     * @throws 如果 Token 无效或过期,则抛出错误
     */
    public verifyToken<T extends JwtPayload | string>(token: string): T {
        try {
            return jwt.verify(token, this.secretKey) as T
        } catch (error: any) {
            throw new Error(`Invalid token: ${error.message}`)
        }
    }

    /**
     * 检查 Token 是否已过期
     * @param token - 要检查的 JWT Token
     * @returns true 表示已过期,false 表示未过期
     */
    public isTokenExpired(token: string): boolean {
        try {
            const decoded = jwt.decode(token, { complete: true })
            if (!decoded || typeof decoded.payload === 'string') {
                return true // 无法解析 Token
            }

            const { exp } = decoded.payload as JwtPayload
            return !!exp && Date.now() >= exp * 1000
        } catch (error) {
            return true // 解析失败则视为已过期
        }
    }
}

const jwtService = new JwtService(process.env.JWT_SECRET_KEY!)
export default jwtService

解决方法

使用 api 请求来使用 redis 和 jwt 功能: